diff --git a/.all-contributorsrc b/.all-contributorsrc
index 7e951953aa..73035ee736 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -123,7 +123,9 @@
"avatar_url": "https://avatars.githubusercontent.com/u/56418363?v=4",
"profile": "https://github.com/devgenix",
"contributions": [
- "code"
+ "code",
+ "example",
+ "doc"
]
},
{
@@ -143,6 +145,188 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "RamchandraWarang9822",
+ "name": "Ramchandra Warang",
+ "avatar_url": "https://avatars.githubusercontent.com/u/92023869?v=4",
+ "profile": "https://github.com/RamchandraWarang9822",
+ "contributions": [
+ "code",
+ "bug"
+ ]
+ },
+ {
+ "login": "lazyfuhrer",
+ "name": "Biswarghya Biswas",
+ "avatar_url": "https://avatars.githubusercontent.com/u/64888892?v=4",
+ "profile": "https://github.com/lazyfuhrer",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "okieLoki",
+ "name": "Uddeepta Raaj Kashyap",
+ "avatar_url": "https://avatars.githubusercontent.com/u/96105929?v=4",
+ "profile": "https://github.com/okieLoki",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "nayeem01",
+ "name": "Nayeem Abdullah",
+ "avatar_url": "https://avatars.githubusercontent.com/u/32274108?v=4",
+ "profile": "http://www.linkedin.com/in/nayeem-abdullah-317098141",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "kangsuhyun-yanolja",
+ "name": "Kang Suhyun",
+ "avatar_url": "https://avatars.githubusercontent.com/u/124246127?v=4",
+ "profile": "https://github.com/kangsuhyun-yanolja",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "yeokyeong-yanolja",
+ "name": "Yoon",
+ "avatar_url": "https://avatars.githubusercontent.com/u/128676129?v=4",
+ "profile": "https://github.com/yeokyeong-yanolja",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "mrkirthi-24",
+ "name": "Kirthi Bagrecha Jain",
+ "avatar_url": "https://avatars.githubusercontent.com/u/53830546?v=4",
+ "profile": "https://mrkirthi24.netlify.app/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "navdeep1840",
+ "name": "Navdeep",
+ "avatar_url": "https://avatars.githubusercontent.com/u/80774259?v=4",
+ "profile": "https://github.com/navdeep1840",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Rhythm-08",
+ "name": "Rhythm Sharma",
+ "avatar_url": "https://avatars.githubusercontent.com/u/64489317?v=4",
+ "profile": "https://www.linkedin.com/in/rhythm-sharma-708a421a8/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "vicradon",
+ "name": "Osinachi Chukwujama ",
+ "avatar_url": "https://avatars.githubusercontent.com/u/40396070?v=4",
+ "profile": "https://osinachi.me",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "morsoli",
+ "name": "莫尔索",
+ "avatar_url": "https://avatars.githubusercontent.com/u/47264881?v=4",
+ "profile": "https://liduos.com/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "dejongbaba",
+ "name": "Agunbiade Adedeji",
+ "avatar_url": "https://avatars.githubusercontent.com/u/22600781?v=4",
+ "profile": "http://luccithedev.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "techemmy",
+ "name": "Emmanuel Oloyede",
+ "avatar_url": "https://avatars.githubusercontent.com/u/43725109?v=4",
+ "profile": "https://techemmy.github.io/",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "Dhaneshwarguiyan",
+ "name": "Dhaneshwarguiyan",
+ "avatar_url": "https://avatars.githubusercontent.com/u/116065351?v=4",
+ "profile": "https://github.com/Dhaneshwarguiyan",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "PentesterPriyanshu",
+ "name": "Priyanshu Prajapati",
+ "avatar_url": "https://avatars.githubusercontent.com/u/98478305?v=4",
+ "profile": "https://github.com/PentesterPriyanshu",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "VenkataRavitejaGullapudi",
+ "name": "Raviteja",
+ "avatar_url": "https://avatars.githubusercontent.com/u/70102577?v=4",
+ "profile": "https://venkataravitejagullapudi.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ArijitCloud",
+ "name": "Arijit",
+ "avatar_url": "https://avatars.githubusercontent.com/u/81144422?v=4",
+ "profile": "https://github.com/ArijitCloud",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "Yachika9925",
+ "name": "Yachika9925",
+ "avatar_url": "https://avatars.githubusercontent.com/u/147185379?v=4",
+ "profile": "https://github.com/Yachika9925",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Dhoni77",
+ "name": "Aldrin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/53973174?v=4",
+ "profile": "https://github.com/Dhoni77",
+ "contributions": [
+ "test"
+ ]
+ },
+ {
+ "login": "seungduk-yanolja",
+ "name": "seungduk.kim.2304",
+ "avatar_url": "https://avatars.githubusercontent.com/u/115020208?v=4",
+ "profile": "https://github.com/seungduk-yanolja",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dd84ea7824..3c14bacab9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,7 +1,7 @@
---
name: Bug report
about: Create a report to help us improve
-title: ''
+title: '[Bug] '
labels: ''
assignees: ''
@@ -34,5 +34,25 @@ If applicable, add screenshots to help explain your problem.
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
-**Additional context**
-Add any other context about the problem here.
+**Important Context**
+Add context about the problem here.
+
+1. **Network Logs in the Browser:**
+ - If applicable, Take a screenshot of the network logs or copy and paste any relevant log entries.
+
+2. **Docker Containers Information:**
+ - If applicable, Provide a screenshot showing the list of running and stopped containers.
+ - Please provide us the with a screenshot of the logs for the agenta-backend docker container.
+
+3. **Additional Information:**
+ - Include any additional details, error messages, or observations that may be helpful.
+
+
+
+
+
+
+
+Please make sure to provide all the requested information to expedite the debugging process. Thank you for helping us improve our project!
+
+---
\ No newline at end of file
diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml
new file mode 100644
index 0000000000..bc322796cc
--- /dev/null
+++ b/.github/workflows/backend_test.yml
@@ -0,0 +1,30 @@
+name: Execute Backend tests
+
+on: [pull_request]
+
+jobs:
+ continous_integration:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Start Docker Compose
+ run: docker-compose -f "docker-compose.test.yml" up -d --build
+
+ - name: Install Curl
+ run: sudo apt install curl -y
+
+ - name: Wait for Backend Service
+ run: |
+ while true; do
+ if curl -s http://localhost:8000/openapi.json; then
+ break
+ fi
+ sleep 5
+ done
+
+ - name: Run tests
+ run: docker exec agenta-backend-test pytest
+
+ - name: Stop Docker Compose
+ run: docker-compose down
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000000..186f6e22cd
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,50 @@
+name: Build and Publish Docker Images
+
+on:
+ push:
+ branches:
+ - main
+
+env:
+ BACKEND_IMAGE_NAME: agenta-backend
+ WEB_IMAGE_NAME: agenta-web
+ IMAGE_OWNER_NAME: agenta-ai
+ REPOSITORY_USERNAME: ${{ github.actor }}
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Clear Docker build cache
+ run: docker builder prune --all --force
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ env.REPOSITORY_USERNAME }}
+ password: ${{ secrets.DOCKER_GITHUB_SECRETS }}
+
+ - name: Build, tag and push Backend image to Github Container Registry
+ id: build-backend-image
+ run: |
+ cd agenta-backend
+ docker build -t ghcr.io/${{ env.IMAGE_OWNER_NAME }}/${{ env.BACKEND_IMAGE_NAME }} -f Dockerfile.gh .
+ docker push ghcr.io/${{ env.IMAGE_OWNER_NAME }}/${{ env.BACKEND_IMAGE_NAME }}
+
+ - name: Build, tag and push Web image to Github Container Registry
+ id: build-web-image
+ run: |
+ cd agenta-web
+ docker build --no-cache -t ghcr.io/${{ env.IMAGE_OWNER_NAME }}/${{ env.WEB_IMAGE_NAME }} -f prod.gh.Dockerfile .
+ docker push ghcr.io/${{ env.IMAGE_OWNER_NAME }}/${{ env.WEB_IMAGE_NAME }}
+
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
new file mode 100644
index 0000000000..d51b2c6f03
--- /dev/null
+++ b/.github/workflows/frontend-tests.yml
@@ -0,0 +1,58 @@
+name: Execute Frontend tests
+
+on: [pull_request]
+env:
+ OPENAI_API_KEY: ${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}
+
+jobs:
+ cypress:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - name: Set Environment Variables
+ run: |
+ echo "NEXT_PUBLIC_OPENAI_API_KEY=${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}" >> $GITHUB_ENV
+
+ - name: Install Curl & Start Docker Compose
+ env:
+ NEXT_PUBLIC_OPENAI_API_KEY: ${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}
+ run: |
+ sudo apt install curl -y
+ docker-compose -f "docker-compose.yml" up -d --build
+
+ - name: Restart Backend Service To Fetch Template(s)
+ run: docker container restart agenta_backend_1
+
+ - name: Wait for Backend Service
+ run: curl http://localhost/api/health/
+
+ - name: Know the Response of Organization & Container Templates
+ run: |
+ curl http://localhost/api/organizations/own/
+ curl http://localhost/api/containers/templates/
+
+ - name: Set Node.js 18
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+
+ - name: Install Frontend Depedencies
+ env:
+ NEXT_PUBLIC_OPENAI_API_KEY: ${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}
+ run: |
+ cd agenta-web/ && npm install
+
+ - name: Run Cypress
+ env:
+ NEXT_PUBLIC_OPENAI_API_KEY: ${{ secrets.NEXT_PUBLIC_OPENAI_API_KEY }}
+ run: cd agenta-web/ && npm run cypress:run
+
+ - name: Docker logs
+ if: always() #
+ run: docker ps -q | xargs -I {} docker logs {}
+
+ - name: Stop Docker Compose
+ run: docker-compose down
diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml
new file mode 100644
index 0000000000..3a6cdaa038
--- /dev/null
+++ b/.github/workflows/lint-and-build.yml
@@ -0,0 +1,30 @@
+name: Lint and Build
+
+on:
+ pull_request:
+ paths:
+ - "agenta-web/**"
+
+jobs:
+ lint_and_build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: 20
+
+ - name: Install dependencies
+ run: npm ci --omit=dev
+ working-directory: agenta-web
+
+ - name: Run Lint
+ run: npm run lint
+ working-directory: agenta-web
+
+ - name: Run Build
+ run: npm run build
+ working-directory: agenta-web
diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml
index 0ce4595c1e..9a582ff5ee 100644
--- a/.github/workflows/prettier.yml
+++ b/.github/workflows/prettier.yml
@@ -17,10 +17,6 @@ jobs:
with:
node-version: 20
- - name: Install dependencies
- run: npm ci
- working-directory: agenta-web
-
- name: Run Prettier
run: npx prettier --check .
working-directory: agenta-web
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000000..31675c3789
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,67 @@
+name: Publish Agenta to PyPI
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set working directory
+ run: cd ${{ github.workspace }}
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+
+ - name: Install poetry
+ run: |
+ curl -sSL https://install.python-poetry.org | python3 -
+
+ - name: Check for agenta version changes
+ id: version_check
+ run: |
+
+ # # Check if there are any tags in the repository
+ # if [ -z "$(git tag -l)" ]; then
+ # echo "No tags found. Skipping version check."
+ # exit 78
+ # fi
+
+ # Navigate to the cli directory
+ cd ${{ github.workspace }}/agenta-cli
+
+ # Verify that the pyproject.toml file exists
+ if [ ! -f pyproject.toml ]; then
+ echo "pyproject.toml file not found. Exiting..."
+ exit 1
+ fi
+
+ previous_version=$(git describe --abbrev=0 --tags)
+ current_version=$(poetry version --no-interaction | awk '{print $2}')
+ if [ "$previous_version" == "$current_version" ]; then
+ echo "No version change detected. Skipping package publication."
+ exit 78
+ fi
+ echo "::set-output name=version_bumped::true"
+ continue-on-error: true
+
+ - name: Build and publish agenta to PyPI
+ if: steps.version_check.outputs.version_bumped == 'true'
+ run: |
+ # Navigate to the cli directory
+ cd ${{ github.workspace }}
+
+ # Build and publish agenta-cli
+ cd agenta-cli
+ poetry build
+ poetry publish --username __token__ --password ${{ secrets.PYPI_API_TOKEN }}
+ continue-on-error: true
diff --git a/.gitignore b/.gitignore
index 7ad3fc3f7a..e1938266ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
/.nox/
/docs/_build/
__pycache__/
-.env
+*.env
env/
tests/poetry.lock
agenta-cli/agenta/templates/main.py
@@ -40,5 +40,18 @@ self-host/*/*.tfstate
self-host/*/*.tfstate.backup
todo.md
-agenta-web/package-lock.json
agenta-web/cypress/videos/
+agenta-backend/agenta_backend/cloud/**
+agenta-web/src/ee/**
+agenta-web/src/config/frontendConfig.ts
+agenta-web/src/config/appInfo.ts
+agenta-web/src/pages/apps/index.tsx
+agenta-web/src/pages/auth/[[...path]].tsx
+docker-compose.cloud.dev.yml
+docker-compose.demo.prod.yml
+docker-compose.demo.dev.yml
+docker-compose.cloud.prod.yml
+agenta-web/src/pages/auth/[[...path]].tsx
+agenta-web/cypress/screenshots/
+agenta-web/cypress/videos/
+.nextjs_cache/
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 63f433914e..5493775e4f 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -2,7 +2,7 @@
## Our Pledge
-We as members, contributors, and leaders pledge to make participation in our
+We as members, contributors, and leaders pledge to participate in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
@@ -15,7 +15,7 @@ diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
-community include:
+community includes:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
@@ -33,7 +33,7 @@ Examples of unacceptable behavior include:
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
+* Other conduct that could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -51,7 +51,7 @@ decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
+an individual is officially representing the community in public areas.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
@@ -87,8 +87,8 @@ of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
+those enforcing the Code of Conduct, for a specified period. This
+includes avoiding interactions in community spaces and external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
@@ -97,8 +97,8 @@ permanent ban.
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
+**Consequence**: A temporary ban from any interaction or public
+communication with the community for a specified period. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d15b6404d6..056f6df223 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,22 +1,43 @@
-# Contributing to agenta
+---
+title: "Contributing to Agenta"
+description: "Guidelines for contributing to the Agenta project"
+---
-Hello, thank you for your interest in contributing to our project. We value your time and effort and hope to make this process as smooth as possible.
+Thanks for your interest in contributing to Agenta! We appreciate your effort and aim to make your contribution experience as straightforward as possible.
## Getting Started
-1. **Local Installation:** Before you start contributing, please install the project locally. You can find the necessary steps in our [Local Installation Guide](https://docs.agenta.ai/installation/local-installation/local-installation).
+1. **Local Installation:** First, set up the project on your local machine. Follow the steps in our [Local Installation Guide](https://docs.agenta.ai/installation/local-installation/local-installation).
-2. **Understanding the Project:** Please take some time to read through our [Conceptual Guide](https://docs.agenta.ai/conceptual/concepts) and [Architecture Guide](https://docs.agenta.ai/conceptual/architecture) to understand the underlying principles and architecture of our project.
+2. **Understand the Project:** Familiarize yourself with our architecture and concepts by reading our [Conceptual Guide](https://docs.agenta.ai/conceptual/concepts) and [Architecture Guide](https://docs.agenta.ai/conceptual/architecture).
-3. **Start Development** Once you have a local installation and a good understanding of the project, you can start contributing. You can read the following tutorial to learn how to run the code in development mode: [Development Mode Tutorial](https://docs.agenta.ai/contributing/development-mode).
+3. **Begin Development:** Once you've installed the project and understand its architecture, you're ready to contribute. See the [Development Mode Tutorial](https://docs.agenta.ai/contributing/development-mode) for instructions on running the code in development mode.
+
+## Code Formatting and Linting
+
+To maintain code quality, we adhere to certain formatting and linting rules:
+
+- **Backend & CLI Formatting with Black:** Use `black` for formatting code in the following directories:
+ - `agenta-backend/**`
+ - `agenta-cli/**`
+ - `examples/**`
+
+ Install `black` with `pip install black`, navigate to the target directory, and run `black .`.
+
+- **Frontend Formatting with Prettier:** We use `prettier` for frontend formatting. Run `npm run format-fix` in the `agenta-web` directory. If you haven't yet installed `prettier`, do so with `npm install prettier`.
## Contribution Steps
-1. **Find an issue:** A good starting point for contributing is to find an existing issue and solve it. Browse our issue tracker, find something that suits your skills, and start coding!
+1. **Pick an Issue:** Start by selecting an issue from our issue tracker. Choose one that matches your skill set and begin coding. For more on this, read our [Creating an Issue Guide](file-issue).
+
+2. **Fork & Pull Request:** Fork our repository, create a new branch, add your changes, and submit a pull request. Ensure your code aligns with our standards and includes appropriate unit tests.
-2. **Fork & Pull Request:** We follow the standard fork & pull request process. Fork our repository, create a new branch, make your changes, and submit a pull request. Make sure your code follows our coding standards and include relevant unit tests.
+3. **Contribute a Larger Feature:** If you're interested in developing a more extensive feature, let's discuss! Contact us directly on Slack or schedule a meeting through this [Calendly link](https://usemotion.com/meet/mahmoud-mabrouk-r0qp/collaborate?d=30).
-3. **Build a Larger Feature:** If you're interested in building a larger feature, we would love to hear from you! Reach out to me directly at opensource@agenta.ai or schedule a meeting using my [Calendly link](https://usemotion.com/meet/mahmoud-mabrouk-r0qp/collaborate?d=30).
+## Contribution Rules
+We had many zombie issues and PRs (assigned but inactive) in the past. We want to avoid this in the future, so we've set up the following rules:
+- An issue may only be assigned to one person for up to one week (three days for very simple issues). If the issue remains unsolved after a week, it will be unassigned and made available to others.
+- Any pull request (PR) left inactive by the author for over a week will be closed. The author can reopen it if they wish to continue.
-Thank you for your interest in contributing to Agenta. We look forward to your pull requests!
+We look forward to seeing your contributions to Agenta!
\ No newline at end of file
diff --git a/README.md b/README.md
index 471029a751..ca4d594c4d 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,23 @@
-
-
-
-
-
-
+
-
-
- Quickly iterate, debug, and evaluate your LLM apps The open-source LLMOps platform for prompt-engineering, evaluation and deployment of complex LLM apps.
-
-
-
-
-
+
+
+ Quickly iterate, debug, and evaluate your LLM apps
+ The open-source LLMOps platform for prompt-engineering, evaluation, human feedback, and deployment of complex LLM apps.
+
+
@@ -29,51 +30,143 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-### **The Open-source Developer-first LLMOps Platform**
+
+
+
+
+---
+
+
+
+---
+
+# ℹ️ About
Building production-ready LLM-powered applications is currently very difficult. It involves countless iterations of prompt engineering, parameter tuning, and architectures.
Agenta provides you with the tools to quickly do prompt engineering and 🧪 **experiment**, ⚖️ **evaluate**, and :rocket: **deploy** your LLM apps. All without imposing any restrictions on your choice of framework, library, or model.
+
+
+
+
+
+
+
+
-https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-67475e4ce9ed
-## Getting Started
+# Demo
+https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-67475e4ce9ed
-Please go to [docs.agenta.ai](https://docs.agenta.ai) for full documentation on:
+# Quick Start
-- [Installation](https://docs.agenta.ai/installation)
-- [Getting Started](https://docs.agenta.ai/getting-started)
-- [Tutorials](https://docs.agenta.ai/tutorials)
-### Deploy to AWS
-
-
+
+
+# Features
+
+Playground 🪄
+ With just a few lines of code, define the parameters and prompts you wish to experiment with. You and your team can quickly experiment and test new variants on the web UI.
+
+https://github.com/Agenta-AI/agenta/assets/4510758/8b736d2b-7c61-414c-b534-d95efc69134c
+
+Version Evaluation 📊
+Define test sets, then evaluate manually or programmatically your different variants.
+
+API Deployment 🚀
+When you are ready, deploy your LLM applications as APIs in one click.
+
+
+
+## Why choose Agenta for building LLM-apps?
+
+- 🔨 **Build quickly**: You need to iterate many times on different architectures and prompts to bring apps to production. We streamline this process and allow you to do this in days instead of weeks.
+- 🏗️ **Build robust apps and reduce hallucination**: We provide you with the tools to systematically and easily evaluate your application to make sure you only serve robust apps to production.
+- 👨💻 **Developer-centric**: We cater to complex LLM-apps and pipelines that require more than one simple prompt. We allow you to experiment and iterate on apps that have complex integration, business logic, and many prompts.
+- 🌐 **Solution-Agnostic**: You have the freedom to use any libraries and models, be it Langchain, llma_index, or a custom-written alternative.
+- 🔒 **Privacy-First**: We respect your privacy and do not proxy your data through third-party services. The platform and the data are hosted on your infrastructure.
+
## How Agenta works:
**1. Write your LLM-app code**
Write the code using any framework, library, or model you want. Add the `agenta.post` decorator and put the inputs and parameters in the function call just like in this example:
-_Example simple application that generates baby names_
+_Example simple application that generates baby names:_
```python
import agenta as ag
@@ -82,19 +175,19 @@ from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
default_prompt = "Give me five cool names for a baby from {country} with this gender {gender}!!!!"
+ag.init()
+ag.config(prompt_template=ag.TextParam(default_prompt),
+ temperature=ag.FloatParam(0.9))
-
-@ag.post
+@ag.entrypoint
def generate(
country: str,
gender: str,
- temperature: ag.FloatParam = 0.9,
- prompt_template: ag.TextParam = default_prompt,
) -> str:
- llm = OpenAI(temperature=temperature)
+ llm = OpenAI(temperature=ag.config.temperature)
prompt = PromptTemplate(
input_variables=["country", "gender"],
- template=prompt_template,
+ template=ag.config.prompt_template,
)
chain = LLMChain(llm=llm, prompt=prompt)
output = chain.run(country=country, gender=gender)
@@ -102,55 +195,43 @@ def generate(
return output
```
-**2.Deploy your app using the Agenta CLI.**
+**2.Deploy your app using the Agenta CLI**
-
+
**3. Go to agenta at localhost:3000**
Now your team can 🔄 iterate, 🧪 experiment, and ⚖️ evaluate different versions of your app (with your code!) in the web platform.
-
-
-## Features
+
-- 🪄 **Playground:** With just a few lines of code, define the parameters and prompts you wish to experiment with. You and your team can quickly experiment and test new variants on the web UI.
-https://github.com/Agenta-AI/agenta/assets/4510758/8b736d2b-7c61-414c-b534-d95efc69134c
+# Support
+Talk with the founders for any commercial inquiries.
+
-- 📊 **Version Evaluation:** Define test sets, the evaluate manually or programmatically your different variants.
+# Disabling Anonymized Tracking
-https://github.com/Agenta-AI/agenta/assets/4510758/f8a2a423-c0a9-40df-9c00-52908cac165a
+To disable anonymized telemetry, set the following environment variable:
-- 🚀 **API Deployment Made Easy:** When you are ready, deploy your LLM applications as APIs in one click.
-
-## Why choose Agenta for building LLM-apps?
-
-- 🔨 **Build quickly**: You need to iterate many times on different architectures and prompts to bring apps to production. We streamline this process and allow you to do this in days instead of weeks.
-- 🏗️ **Build robust apps and reduce hallucination**: We provide you with the tools to systematically and easily evaluate your application to make sure you only serve robust apps to production
-- 👨💻 **Developer-centric**: We cater to complex LLM-apps and pipelines that require more than one simple prompt. We allow you to experiment and iterate on apps that have complex integration, business logic, and many prompts.
-- 🌐 **Solution-Agnostic**: You have the freedom to use any library and models, be it Langchain, llma_index, or a custom-written alternative.
-- 🔒 **Privacy-First**: We respect your privacy and do not proxy your data through third-party services. The platform and the data are hosted on your infrastructure.
+- For web: Set `TELEMETRY_TRACKING_ENABLED` to `false` in your `agenta-web/.env` file.
+- For CLI: Set `telemetry_tracking_enabled` to `false` in your `~/.agenta/config.toml` file.
+After making this change, restart agenta compose.
-## Onboarding and support
-
-Are you interested in using Agenta in your business? We'd love to talk to you about your needs and show you how agenta can help.
-[Book a meeting with the founders here](https://cal.com/mahmoud-mabrouk-ogzgey/demo) to start the conversation.
-
-## Contributing
+# Contributing
We warmly welcome contributions to Agenta. Feel free to submit issues, fork the repository, and send pull requests.
-We are usually hanging in our Slack. Feel free to [join our slack and ask us anything](https://join.slack.com/t/agenta-hq/shared_invite/zt-1zsafop5i-Y7~ZySbhRZvKVPV5DO_7IA)
+We are usually hanging in our Slack. Feel free to [join our Slack and ask us anything](https://join.slack.com/t/agenta-hq/shared_invite/zt-1zsafop5i-Y7~ZySbhRZvKVPV5DO_7IA)
Check out our [Contributing Guide](https://docs.agenta.ai/contributing/getting-started) for more information.
## Contributors ✨
-[](#contributors-)
+[](#contributors-)
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -174,10 +255,36 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Kaosi Ezealigo 🐛 💻
Alberto Nunes 🐛
Maaz Bin Khawar 💻 👀 🧑🏫
- Nehemiah Onyekachukwu Emmanuel 💻
+ Nehemiah Onyekachukwu Emmanuel 💻 💡 📖
Philip Okiokio 📖
Abhinav Pandey 💻
+
+ Ramchandra Warang 💻 🐛
+ Biswarghya Biswas 💻
+ Uddeepta Raaj Kashyap 💻
+ Nayeem Abdullah 💻
+ Kang Suhyun 💻
+ Yoon 💻
+ Kirthi Bagrecha Jain 💻
+
+
+ Navdeep 💻
+ Rhythm Sharma 💻
+ Osinachi Chukwujama 💻
+ 莫尔索 📖
+ Agunbiade Adedeji 💻
+ Emmanuel Oloyede 💻 📖
+ Dhaneshwarguiyan 💻
+
+
+ Priyanshu Prajapati 📖
+ Raviteja 💻
+ Arijit 💻
+ Yachika9925 📖
+ Aldrin ⚠️
+ seungduk.kim.2304 💻
+
@@ -186,6 +293,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
-This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
+This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome!
-Attribution: Testing icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/testing)
+**Attribution**: Testing icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/testing)
diff --git a/agenta-backend/Dockerfile b/agenta-backend/Dockerfile
index 8e0cf86ec8..db08614993 100644
--- a/agenta-backend/Dockerfile
+++ b/agenta-backend/Dockerfile
@@ -8,7 +8,7 @@ RUN pip install --upgrade pip \
&& pip install poetry
# Copy only requirements to cache them in docker layer
-COPY pyproject.toml poetry.lock* README.md sleep.py /app/
+COPY pyproject.toml poetry.lock* README.md /app/
# This is a hack to create a dummy module so that poetry install doesn't fail
RUN mkdir -p /app/agenta_backend
@@ -17,6 +17,7 @@ RUN touch /app/agenta_backend/__init__.py
# Project initialization:
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
+
# remove dummy module
RUN rm -r /app/agenta_backend
EXPOSE 8000
\ No newline at end of file
diff --git a/agenta-backend/Dockerfile.gh b/agenta-backend/Dockerfile.gh
new file mode 100644
index 0000000000..cad7f2b20d
--- /dev/null
+++ b/agenta-backend/Dockerfile.gh
@@ -0,0 +1,18 @@
+# Dockerfile
+FROM python:3.9-slim-buster
+
+WORKDIR /app
+
+# Ensure pip and poetry are up to date
+RUN pip install --upgrade pip \
+ && pip install poetry
+
+# Copy agenta_backend to app
+COPY . /app/
+
+# Project initialization:
+RUN poetry config virtualenvs.create false \
+ && poetry install --no-interaction --no-ansi
+
+# Expose 8000
+EXPOSE 8000
\ No newline at end of file
diff --git a/agenta-backend/agenta_backend/__init__.py b/agenta-backend/agenta_backend/__init__.py
index e69de29bb2..1f17607a17 100644
--- a/agenta-backend/agenta_backend/__init__.py
+++ b/agenta-backend/agenta_backend/__init__.py
@@ -0,0 +1,4 @@
+import os
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ import agenta_backend.cloud.__init__
diff --git a/agenta-backend/agenta_backend/config.py b/agenta-backend/agenta_backend/config.py
index 9d574d386a..56965077b3 100644
--- a/agenta-backend/agenta_backend/config.py
+++ b/agenta-backend/agenta_backend/config.py
@@ -2,6 +2,7 @@
import os
import toml
+from typing import Optional
# Load the settings from the .toml file
toml_config = toml.load("agenta_backend/config.toml")
@@ -13,13 +14,12 @@
os.environ["DOCKER_HUB_URL"] = toml_config["docker_hub_url"]
os.environ["DOCKER_HUB_REPO_OWNER"] = toml_config["docker_hub_repo_owner"]
os.environ["DOCKER_HUB_REPO_NAME"] = toml_config["docker_hub_repo_name"]
-os.environ["REDIS_URL"] = toml_config["redis_url"]
+os.environ["DATABASE_MODE"] = os.environ.get("DATABASE_MODE", "v2")
class Settings(BaseSettings):
docker_registry_url: str
registry: str
- redis_url: str
database_url: str
docker_hub_url: str
docker_hub_repo_owner: str
diff --git a/agenta-backend/agenta_backend/config.toml b/agenta-backend/agenta_backend/config.toml
index 1c9d2f2fbd..c5b0f0b5aa 100644
--- a/agenta-backend/agenta_backend/config.toml
+++ b/agenta-backend/agenta_backend/config.toml
@@ -1,7 +1,6 @@
docker_registry_url="registry:5001"
registry="agentaai"
database_url="sqlite:////db/deploy.db"
-redis_url="redis://redis:6379/0"
docker_hub_url="https://hub.docker.com/v2/repositories/{}/{}"
docker_hub_repo_owner="agentaai"
-docker_hub_repo_name="templates"
\ No newline at end of file
+docker_hub_repo_name="templates_v2"
diff --git a/agenta-backend/agenta_backend/ee b/agenta-backend/agenta_backend/ee
new file mode 120000
index 0000000000..567193fbc8
--- /dev/null
+++ b/agenta-backend/agenta_backend/ee
@@ -0,0 +1 @@
+../ee_backend/ee
\ No newline at end of file
diff --git a/agenta-backend/agenta_backend/main.py b/agenta-backend/agenta_backend/main.py
index b20bb3660e..ee083eea25 100644
--- a/agenta-backend/agenta_backend/main.py
+++ b/agenta-backend/agenta_backend/main.py
@@ -1,23 +1,29 @@
-import json
-from fastapi import FastAPI
+import os
+from contextlib import asynccontextmanager
-from agenta_backend.routers import app_variant
-from agenta_backend.routers import testset_router
-from fastapi.middleware.cors import CORSMiddleware
-from agenta_backend.routers import container_router
-from agenta_backend.routers import evaluation_router
-from agenta_backend.services.db_manager import (
- add_template,
- remove_old_template_from_db,
+from agenta_backend.config import settings
+from agenta_backend.routers import (
+ app_router,
+ container_router,
+ environment_router,
+ evaluation_router,
+ observability_router,
+ organization_router,
+ testset_router,
+ user_profile,
+ variants_router,
+ bases_router,
+ configs_router,
+ health_router,
)
-from agenta_backend.services.cache_manager import (
- retrieve_templates_from_dockerhub_cached,
- retrieve_templates_info_from_dockerhub_cached,
-)
-
-from contextlib import asynccontextmanager
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services import templates_manager
+else:
+ from agenta_backend.services import templates_manager
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
origins = [
"http://localhost:3000",
@@ -35,53 +41,41 @@ async def lifespan(application: FastAPI, cache=True):
application: FastAPI application.
cache: A boolean value that indicates whether to use the cached data or not.
"""
- tags_data = await retrieve_templates_from_dockerhub_cached(cache=cache)
- templates_info_string = await retrieve_templates_info_from_dockerhub_cached(
- cache=cache
- )
- templates_info = json.loads(templates_info_string)
-
- templates_in_hub = []
- for tag in tags_data:
- # Append the template id in the list of templates_in_hub
- # We do this to remove old templates from database
- templates_in_hub.append(tag["id"])
- for temp_info_key in templates_info:
- temp_info = templates_info[temp_info_key]
- if str(tag["name"]).startswith(temp_info_key):
- add_template(
- **{
- "template_id": tag["id"],
- "name": tag["name"],
- "size": tag["images"][0]["size"],
- "architecture": tag["images"][0]["architecture"],
- "title": temp_info["name"],
- "description": temp_info["description"],
- "digest": tag["digest"],
- "status": tag["images"][0]["status"],
- "last_pushed": tag["images"][0]["last_pushed"],
- "repo_name": tag["last_updater_username"],
- "media_type": tag["media_type"],
- }
- )
- print(f"Template {tag['id']} added to the database.")
-
- # Remove old templates from database
- remove_old_template_from_db(templates_in_hub)
+ await templates_manager.update_and_sync_templates(cache=cache)
yield
-# this is the prefix in which we are reverse proxying the api
app = FastAPI(lifespan=lifespan)
-app.include_router(app_variant.router, prefix="/app_variant")
-app.include_router(evaluation_router.router, prefix="/evaluations")
-app.include_router(testset_router.router, prefix="/testsets")
-app.include_router(container_router.router, prefix="/containers")
+
+allow_headers = ["Content-Type"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
- allow_headers=["*"],
+ allow_headers=allow_headers,
)
+
+if os.environ["FEATURE_FLAG"] not in ["cloud", "ee"]:
+ from agenta_backend.services.auth_helper import authentication_middleware
+
+ app.middleware("http")(authentication_middleware)
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ import agenta_backend.cloud.main as cloud
+
+ app, allow_headers = cloud.extend_main(app)
+
+app.include_router(health_router.router, prefix="/health")
+app.include_router(user_profile.router, prefix="/profile")
+app.include_router(app_router.router, prefix="/apps")
+app.include_router(variants_router.router, prefix="/variants")
+app.include_router(evaluation_router.router, prefix="/evaluations")
+app.include_router(testset_router.router, prefix="/testsets")
+app.include_router(container_router.router, prefix="/containers")
+app.include_router(environment_router.router, prefix="/environments")
+app.include_router(observability_router.router, prefix="/observability")
+app.include_router(organization_router.router, prefix="/organizations")
+app.include_router(bases_router.router, prefix="/bases")
+app.include_router(configs_router.router, prefix="/configs")
diff --git a/agenta-backend/agenta_backend/models/api/api_models.py b/agenta-backend/agenta_backend/models/api/api_models.py
index 1c9ebde272..8bd6aef44a 100644
--- a/agenta-backend/agenta_backend/models/api/api_models.py
+++ b/agenta-backend/agenta_backend/models/api/api_models.py
@@ -1,35 +1,146 @@
from datetime import datetime
+from typing import Any, Dict, List, Optional
+from enum import Enum
+
from pydantic import BaseModel
-from typing import List, Optional, Dict, Any
+
+
+class GetConfigReponse(BaseModel):
+ config_id: str
+ config_name: str
+ current_version: int
+ parameters: Dict[str, Any]
+
+
+class SaveConfigPayload(BaseModel):
+ base_id: str
+ config_name: str
+ parameters: Dict[str, Any]
+ overwrite: bool
+
+
+class VariantActionEnum(str, Enum):
+ START = "START"
+ STOP = "STOP"
+
+
+class VariantAction(BaseModel):
+ action: VariantActionEnum
+
+
+class CreateApp(BaseModel):
+ app_name: str
+ organization_id: Optional[str] = None
+
+
+class CreateAppOutput(BaseModel):
+ app_id: str
+ app_name: str
+
+
+class AppOutput(CreateAppOutput):
+ pass
+
+
+class UpdateVariantParameterPayload(BaseModel):
+ parameters: Dict[str, Any]
class AppVariant(BaseModel):
+ app_id: str
app_name: str
variant_name: str
parameters: Optional[Dict[str, Any]]
previous_variant_name: Optional[str]
+ organization_id: Optional[str] = None
+ base_name: Optional[str]
+ config_name: Optional[str]
+
+
+class AppVariantFromImagePayload(BaseModel):
+ variant_name: str
+
+
+class AppVariantOutput(BaseModel):
+ app_id: str
+ app_name: str
+ variant_id: str
+ variant_name: str
+ parameters: Optional[Dict[str, Any]]
+ previous_variant_name: Optional[str]
+ organization_id: str
+ user_id: str
+ base_name: str
+ base_id: str
+ config_name: str
+ config_id: str
+ uri: Optional[str]
+
+
+class EnvironmentOutput(BaseModel):
+ name: str
+ app_id: str
+ deployed_app_variant_id: Optional[str]
+ deployed_variant_name: Optional[str]
+
+
+class AddVariantFromPreviousPayload(BaseModel):
+ new_variant_name: str
+ parameters: Dict[str, Any]
+
+
+class AddVariantFromBasePayload(BaseModel):
+ base_id: str
+ new_variant_name: str
+ new_config_name: str
+ parameters: Dict[str, Any]
+
+
+class AppVariantFromImage(BaseModel):
+ app_id: str
+ variant_name: str
+ parameters: Optional[Dict[str, Any]]
+ previous_variant_name: Optional[str]
+ organization_id: Optional[str] = None
+
+
+class RestartAppContainer(BaseModel):
+ variant_id: str
class Image(BaseModel):
+ type: Optional[str]
+ docker_id: str
+ tags: str
+ organization_id: Optional[str] = None
+
+
+class AddVariantFromImagePayload(BaseModel):
+ variant_name: str
docker_id: str
tags: str
+ base_name: Optional[str]
+ config_name: Optional[str]
+
+
+class ImageExtended(Image):
+ # includes the mongodb image id
+ id: str
class TemplateImageInfo(BaseModel):
name: str
- size: int
- digest: str
- status: str
- architecture: str
+ size: Optional[int] = None
+ digest: Optional[str] = None
title: str
description: str
- last_pushed: datetime
- repo_name: str
- media_type: str
+ last_pushed: Optional[datetime] = None
+ repo_name: Optional[str] = None
+ template_uri: Optional[str] = None
class Template(BaseModel):
- id: int
+ id: str
image: TemplateImageInfo
@@ -38,15 +149,69 @@ class URI(BaseModel):
class App(BaseModel):
+ app_id: str
app_name: str
+class RemoveApp(BaseModel):
+ app_id: str
+
+
class DockerEnvVars(BaseModel):
env_vars: Dict[str, str]
class CreateAppVariant(BaseModel):
app_name: str
- image_id: str
- image_tag: str
+ template_id: str
env_vars: Dict[str, str]
+ organization_id: Optional[str] = None
+
+
+class InviteRequest(BaseModel):
+ email: str
+
+
+class InviteToken(BaseModel):
+ token: str
+
+
+class Environment(BaseModel):
+ name: str
+ deployed_app_variant: Optional[str]
+ deployed_base_name: Optional[str]
+ deployed_config_name: Optional[str]
+ organization_id: Optional[str] = None
+
+
+class DeployToEnvironmentPayload(BaseModel):
+ environment_name: str
+ variant_id: str
+
+
+class TestSetOutput(BaseModel):
+ id: str
+ name: str
+ csvdata: List[Dict[str, Any]]
+ created_at: str
+ updated_at: str
+
+
+class PostVariantConfigPayload(BaseModel):
+ app_name: str
+ base_name: str
+ config_name: str
+ parameters: Dict[str, Any]
+ overwrite: bool
+
+
+class ListAPIKeysOutput(BaseModel):
+ prefix: str
+ created_at: datetime
+ last_used_at: datetime = None
+ expiration_date: datetime = None
+
+
+class BaseOutput(BaseModel):
+ base_id: str
+ base_name: str
diff --git a/agenta-backend/agenta_backend/models/api/evaluation_model.py b/agenta-backend/agenta_backend/models/api/evaluation_model.py
index 3a2fa3c5c2..4edf312d38 100644
--- a/agenta-backend/agenta_backend/models/api/evaluation_model.py
+++ b/agenta-backend/agenta_backend/models/api/evaluation_model.py
@@ -1,19 +1,29 @@
-from pydantic import BaseModel, Field
-from typing import Optional, List, Dict
-from datetime import datetime
from enum import Enum
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any, Union
class EvaluationTypeSettings(BaseModel):
similarity_threshold: Optional[float]
+ regex_pattern: Optional[str]
+ regex_should_match: Optional[bool]
+ webhook_url: Optional[str]
+ custom_code_evaluation_id: Optional[str]
+ llm_app_prompt_template: Optional[str]
+ evaluation_prompt_template: Optional[str]
class EvaluationType(str, Enum):
auto_exact_match = "auto_exact_match"
auto_similarity_match = "auto_similarity_match"
+ auto_regex_test = "auto_regex_test"
+ auto_webhook_test = "auto_webhook_test"
auto_ai_critique = "auto_ai_critique"
human_a_b_testing = "human_a_b_testing"
human_scoring = "human_scoring"
+ custom_code_run = "custom_code_run"
+ single_model_test = "single_model_test"
class EvaluationStatusEnum(str, Enum):
@@ -23,62 +33,129 @@ class EvaluationStatusEnum(str, Enum):
EVALUATION_FINISHED = "EVALUATION_FINISHED"
-class EvaluationStatus(BaseModel):
- status: EvaluationStatusEnum
-
-
class Evaluation(BaseModel):
id: str
- status: str
+ app_id: str
+ user_id: str
+ user_username: str
evaluation_type: EvaluationType
evaluation_type_settings: Optional[EvaluationTypeSettings]
- llm_app_prompt_template: Optional[str]
- variants: Optional[List[str]]
- app_name: str
- testset: Dict[str, str] = Field(...)
+ variant_ids: List[str]
+ variant_names: List[str]
+ testset_id: str
+ testset_name: str
+ status: str
created_at: datetime
updated_at: datetime
+class SimpleEvaluationOutput(BaseModel):
+ id: str
+ variant_ids: List[str]
+ app_id: str
+ status: str
+ evaluation_type: EvaluationType
+
+
+class EvaluationUpdate(BaseModel):
+ status: Optional[EvaluationStatusEnum]
+ evaluation_type_settings: Optional[EvaluationTypeSettings]
+
+
class EvaluationScenarioInput(BaseModel):
input_name: str
input_value: str
class EvaluationScenarioOutput(BaseModel):
- variant_name: str
+ variant_id: str
variant_output: str
class EvaluationScenario(BaseModel):
+ id: Optional[str]
evaluation_id: str
inputs: List[EvaluationScenarioInput]
outputs: List[EvaluationScenarioOutput]
vote: Optional[str]
- score: Optional[str]
+ score: Optional[Union[str, int]]
evaluation: Optional[str]
correct_answer: Optional[str]
- id: Optional[str]
+ is_pinned: Optional[bool]
+ note: Optional[str]
-class EvaluationScenarioUpdate(BaseModel):
- vote: Optional[str]
- score: Optional[str]
+class AICritiqueCreate(BaseModel):
+ correct_answer: str
+ llm_app_prompt_template: Optional[str]
+ inputs: List[EvaluationScenarioInput]
outputs: List[EvaluationScenarioOutput]
evaluation_prompt_template: Optional[str]
open_ai_key: Optional[str]
+class EvaluationScenarioUpdate(BaseModel):
+ vote: Optional[str]
+ score: Optional[Union[str, int]]
+ correct_answer: Optional[str] # will be used when running custom code evaluation
+ outputs: Optional[List[EvaluationScenarioOutput]]
+ inputs: Optional[List[EvaluationScenarioInput]]
+ is_pinned: Optional[bool]
+ note: Optional[str]
+
+
+class EvaluationScenarioScoreUpdate(BaseModel):
+ score: float
+
+
class NewEvaluation(BaseModel):
+ app_id: str
+ variant_ids: List[str]
evaluation_type: EvaluationType
evaluation_type_settings: Optional[EvaluationTypeSettings]
- app_name: str
- variants: List[str]
inputs: List[str]
- testset: Dict[str, str] = Field(...)
- status: str = Field(...)
- llm_app_prompt_template: Optional[str]
+ testset_id: str
+ status: str
class DeleteEvaluation(BaseModel):
evaluations_ids: List[str]
+
+
+class CreateCustomEvaluation(BaseModel):
+ evaluation_name: str
+ python_code: str
+ app_id: str
+
+
+class CustomEvaluationOutput(BaseModel):
+ id: str
+ app_id: str
+ evaluation_name: str
+ created_at: datetime
+
+
+class CustomEvaluationDetail(BaseModel):
+ id: str
+ app_id: str
+ evaluation_name: str
+ python_code: str
+ created_at: datetime
+ updated_at: datetime
+
+
+class CustomEvaluationNames(BaseModel):
+ id: str
+ evaluation_name: str
+
+
+class ExecuteCustomEvaluationCode(BaseModel):
+ inputs: List[Dict[str, Any]]
+ app_id: str
+ variant_id: str
+ correct_answer: str
+ outputs: List[Dict[str, Any]]
+
+
+class EvaluationWebhook(BaseModel):
+ score: float
diff --git a/agenta-backend/agenta_backend/models/api/observability_models.py b/agenta-backend/agenta_backend/models/api/observability_models.py
new file mode 100644
index 0000000000..cc6cb4233f
--- /dev/null
+++ b/agenta-backend/agenta_backend/models/api/observability_models.py
@@ -0,0 +1,74 @@
+from datetime import datetime
+from typing import List, Optional, Dict, Any, Union
+
+from pydantic import BaseModel
+
+
+class BaseSpan(BaseModel):
+ parent_span_id: Optional[str]
+ meta: Optional[Dict[str, Any]]
+ event_name: str
+ event_type: Optional[str]
+ start_time: datetime
+ duration: Optional[int]
+ status: str
+ end_time: datetime
+ inputs: Optional[List[str]]
+ outputs: Optional[List[str]]
+ prompt_template: Optional[str]
+ tokens_input: Optional[int]
+ tokens_output: Optional[int]
+ token_total: Optional[int]
+ cost: Optional[float]
+ tags: Optional[List[str]]
+
+
+class CreateSpan(BaseSpan):
+ pass
+
+
+class Span(BaseSpan):
+ span_id: str
+
+
+class CreateFeedback(BaseModel):
+ feedback: Optional[str]
+ score: Optional[float]
+ meta: Optional[Dict]
+
+
+class Feedback(CreateFeedback):
+ feedback_id: str
+ created_at: Optional[datetime]
+
+
+class UpdateFeedback(BaseModel):
+ feedback: str
+ score: Optional[float]
+ meta: Optional[Dict]
+
+
+class BaseTrace(BaseModel):
+ app_id: Optional[str]
+ variant_id: Optional[str]
+ cost: Optional[float]
+ latency: float
+ status: str
+ token_consumption: Optional[int]
+ tags: Optional[List[str]]
+ start_time: datetime
+ end_time: datetime
+
+
+class Trace(BaseTrace):
+ trace_id: str
+ spans: List[str]
+ feedbacks: Optional[List[Feedback]]
+
+
+class CreateTrace(BaseTrace):
+ spans: List[str]
+
+
+class UpdateTrace(BaseModel):
+ status: str
diff --git a/agenta-backend/agenta_backend/models/api/organization_models.py b/agenta-backend/agenta_backend/models/api/organization_models.py
new file mode 100644
index 0000000000..c4151d697d
--- /dev/null
+++ b/agenta-backend/agenta_backend/models/api/organization_models.py
@@ -0,0 +1,29 @@
+from datetime import datetime
+from bson import ObjectId
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+class TimestampModel(BaseModel):
+ created_at: datetime = Field(datetime.utcnow())
+ updated_at: datetime = Field(datetime.utcnow())
+
+
+class Organization(BaseModel):
+ id: Optional[str]
+ name: str
+ description: Optional[str]
+ type: Optional[str]
+ owner: str
+ members: Optional[List[str]]
+ invitations: Optional[List]
+
+
+class OrganizationUpdate(BaseModel):
+ name: Optional[str]
+ description: Optional[str]
+
+
+class OrganizationOutput(BaseModel):
+ id: str
+ name: str
diff --git a/agenta-backend/agenta_backend/models/api/testset_model.py b/agenta-backend/agenta_backend/models/api/testset_model.py
index 039b7f08c8..272fd15215 100644
--- a/agenta-backend/agenta_backend/models/api/testset_model.py
+++ b/agenta-backend/agenta_backend/models/api/testset_model.py
@@ -1,5 +1,6 @@
-from pydantic import BaseModel, Field
+from datetime import datetime
from typing import Any, List, Dict
+from pydantic import BaseModel, Field
class TestsetModel(BaseModel):
@@ -19,7 +20,7 @@ class Config:
}
-class UploadResponse(BaseModel):
+class TestSetSimpleResponse(BaseModel):
id: str
name: str
created_at: str
@@ -41,3 +42,12 @@ class DeleteTestsets(BaseModel):
class NewTestset(BaseModel):
name: str
csvdata: List[Dict[str, str]]
+
+
+class TestSetOutputResponse(BaseModel):
+ id: str = Field(..., alias="_id")
+ name: str
+ created_at: datetime
+
+ class Config:
+ allow_population_by_field_name = True
diff --git a/agenta-backend/agenta_backend/models/api/user_models.py b/agenta-backend/agenta_backend/models/api/user_models.py
new file mode 100644
index 0000000000..21cf68ce1e
--- /dev/null
+++ b/agenta-backend/agenta_backend/models/api/user_models.py
@@ -0,0 +1,22 @@
+from datetime import datetime
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+class TimestampModel(BaseModel):
+ created_at: datetime = Field(datetime.utcnow())
+ updated_at: datetime = Field(datetime.utcnow())
+
+
+class User(TimestampModel):
+ id: Optional[str]
+ uid: str
+ username: str
+ email: str # switch to EmailStr when langchain support pydantic>=2.1
+ organizations: Optional[List[str]]
+
+
+class UserUpdate(BaseModel):
+ username: Optional[str]
+ email: Optional[str]
+ updated_at: datetime = Field(datetime.utcnow())
diff --git a/agenta-backend/agenta_backend/models/converters.py b/agenta-backend/agenta_backend/models/converters.py
index 449f891689..aca21d24d5 100644
--- a/agenta-backend/agenta_backend/models/converters.py
+++ b/agenta-backend/agenta_backend/models/converters.py
@@ -1,46 +1,290 @@
"""Converts db models to pydantic models
"""
from typing import List
-from agenta_backend.models.db_models import AppVariantDB, ImageDB, TemplateDB
+from agenta_backend.services import db_manager
+from agenta_backend.models.api.user_models import User
+from agenta_backend.models.db_models import (
+ AppVariantDB,
+ ImageDB,
+ TemplateDB,
+ AppDB,
+ AppEnvironmentDB,
+ TestSetDB,
+ SpanDB,
+ TraceDB,
+ Feedback as FeedbackDB,
+ EvaluationDB,
+ EvaluationScenarioDB,
+ VariantBaseDB,
+ UserDB,
+)
from agenta_backend.models.api.api_models import (
AppVariant,
- Image,
+ ImageExtended,
Template,
TemplateImageInfo,
+ AppVariantOutput,
+ App,
+ EnvironmentOutput,
+ TestSetOutput,
+ BaseOutput,
+)
+from agenta_backend.models.api.observability_models import (
+ Span,
+ Trace,
+ Feedback as FeedbackOutput,
+)
+from agenta_backend.models.api.evaluation_model import (
+ SimpleEvaluationOutput,
+ EvaluationScenario,
+ Evaluation,
)
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+def evaluation_db_to_simple_evaluation_output(
+ evaluation_db: EvaluationDB,
+) -> SimpleEvaluationOutput:
+ return SimpleEvaluationOutput(
+ id=str(evaluation_db.id),
+ app_id=str(evaluation_db.app.id),
+ status=evaluation_db.status,
+ evaluation_type=evaluation_db.evaluation_type,
+ variant_ids=[str(variant) for variant in evaluation_db.variants],
+ )
+
+
+async def evaluation_db_to_pydantic(
+ evaluation_db: EvaluationDB,
+) -> Evaluation:
+ variant_names = []
+ for variant_id in evaluation_db.variants:
+ variant = await db_manager.get_app_variant_instance_by_id(str(variant_id))
+ variant_name = variant.variant_name if variant else str(variant_id)
+ variant_names.append(str(variant_name))
+
+ return Evaluation(
+ id=str(evaluation_db.id),
+ app_id=str(evaluation_db.app.id),
+ user_id=str(evaluation_db.user.id),
+ user_username=evaluation_db.user.username or "",
+ status=evaluation_db.status,
+ evaluation_type=evaluation_db.evaluation_type,
+ evaluation_type_settings=evaluation_db.evaluation_type_settings,
+ variant_ids=[str(variant) for variant in evaluation_db.variants],
+ variant_names=variant_names,
+ testset_id=str(evaluation_db.testset.id),
+ testset_name=evaluation_db.testset.name,
+ created_at=evaluation_db.created_at,
+ updated_at=evaluation_db.updated_at,
+ )
+
+
+def evaluation_scenario_db_to_pydantic(
+ evaluation_scenario_db: EvaluationScenarioDB,
+) -> EvaluationScenario:
+ return EvaluationScenario(
+ id=str(evaluation_scenario_db.id),
+ evaluation_id=str(evaluation_scenario_db.evaluation.id),
+ inputs=evaluation_scenario_db.inputs,
+ outputs=evaluation_scenario_db.outputs,
+ vote=evaluation_scenario_db.vote,
+ score=evaluation_scenario_db.score,
+ correct_answer=evaluation_scenario_db.correct_answer,
+ is_pinned=evaluation_scenario_db.is_pinned or False,
+ note=evaluation_scenario_db.note or "",
+ )
+
def app_variant_db_to_pydantic(
app_variant_db: AppVariantDB, previous_variant_name: str = None
) -> AppVariant:
return AppVariant(
- app_name=app_variant_db.app_name,
+ app_id=str(app_variant_db.app.id),
+ app_name=app_variant_db.app.app_name,
+ variant_name=app_variant_db.variant_name,
+ parameters=app_variant_db.parameters,
+ previous_variant_name=app_variant_db.previous_variant_name,
+ organization_id=str(app_variant_db.organization.id),
+ base_name=app_variant_db.base_name,
+ config_name=app_variant_db.config_name,
+ )
+
+
+async def app_variant_db_to_output(app_variant_db: AppVariantDB) -> AppVariantOutput:
+ if app_variant_db.base.deployment:
+ deployment = await db_manager.get_deployment_by_objectid(
+ app_variant_db.base.deployment
+ )
+ uri = deployment.uri
+ else:
+ deployment = None
+ uri = None
+ logger.info(f"uri: {uri} deployment: {app_variant_db.base.deployment} {deployment}")
+ return AppVariantOutput(
+ app_id=str(app_variant_db.app.id),
+ app_name=str(app_variant_db.app.app_name),
variant_name=app_variant_db.variant_name,
+ variant_id=str(app_variant_db.id),
+ user_id=str(app_variant_db.user.id),
+ organization_id=str(app_variant_db.organization.id),
parameters=app_variant_db.parameters,
previous_variant_name=app_variant_db.previous_variant_name,
+ base_name=app_variant_db.base_name,
+ base_id=str(app_variant_db.base.id),
+ config_name=app_variant_db.config_name,
+ config_id=str(app_variant_db.config.id),
+ uri=uri,
)
-def image_db_to_pydantic(image_db: ImageDB) -> Image:
- return Image(docker_id=image_db.docker_id, tags=image_db.tags)
+async def environment_db_to_output(
+ environment_db: AppEnvironmentDB,
+) -> EnvironmentOutput:
+ deployed_app_variant_id = (
+ str(environment_db.deployed_app_variant)
+ if environment_db.deployed_app_variant
+ else None
+ )
+ if deployed_app_variant_id:
+ deployed_variant_name = (
+ await db_manager.get_app_variant_instance_by_id(deployed_app_variant_id)
+ ).variant_name
+ else:
+ deployed_variant_name = None
+ return EnvironmentOutput(
+ name=environment_db.name,
+ app_id=str(environment_db.app.id),
+ deployed_app_variant_id=deployed_app_variant_id,
+ deployed_variant_name=deployed_variant_name,
+ )
+
+
+def base_db_to_pydantic(base_db: VariantBaseDB) -> BaseOutput:
+ return BaseOutput(base_id=str(base_db.id), base_name=base_db.base_name)
+
+
+def app_db_to_pydantic(app_db: AppDB) -> App:
+ return App(app_name=app_db.app_name, app_id=str(app_db.id))
+
+
+def image_db_to_pydantic(image_db: ImageDB) -> ImageExtended:
+ return ImageExtended(
+ organization_id=str(image_db.organization.id),
+ docker_id=image_db.docker_id,
+ tags=image_db.tags,
+ id=str(image_db.id),
+ )
def templates_db_to_pydantic(templates_db: List[TemplateDB]) -> List[Template]:
return [
Template(
- id=template.id,
+ id=str(template.id),
image=TemplateImageInfo(
name=template.name,
- size=template.size,
- digest=template.digest,
+ size=template.size if template.size else None,
+ digest=template.digest if template.digest else None,
title=template.title,
description=template.description,
- architecture=template.architecture,
- status=template.status,
- last_pushed=template.last_pushed,
- repo_name=template.repo_name,
- media_type=template.media_type,
+ last_pushed=template.last_pushed if template.last_pushed else None,
+ repo_name=template.repo_name if template.repo_name else None,
+ template_uri=template.template_uri if template.template_uri else None,
),
)
for template in templates_db
]
+
+
+def testset_db_to_pydantic(test_set_db: TestSetDB) -> TestSetOutput:
+ """
+ Convert a TestSetDB object to a TestSetAPI object.
+
+ Args:
+ test_set_db (Dict): The TestSetDB object to be converted.
+
+ Returns:
+ TestSetAPI: The converted TestSetAPI object.
+ """
+ return TestSetOutput(
+ name=test_set_db.name,
+ csvdata=test_set_db.csvdata,
+ created_at=str(test_set_db.created_at),
+ updated_at=str(test_set_db.updated_at),
+ id=str(test_set_db.id),
+ )
+
+
+def spans_db_to_pydantic(spans_db: List[SpanDB]) -> List[Span]:
+ return [
+ Span(
+ span_id=str(span_db.id),
+ parent_span_id=str(span_db.parent_span_id),
+ meta=span_db.meta,
+ event_name=span_db.event_name,
+ event_type=span_db.event_type,
+ start_time=span_db.start_time,
+ duration=span_db.duration,
+ status=span_db.status,
+ end_time=span_db.end_time,
+ inputs=span_db.inputs,
+ outputs=span_db.outputs,
+ prompt_template=span_db.prompt_template,
+ tokens_input=span_db.tokens_input,
+ tokens_output=span_db.tokens_output,
+ token_total=span_db.token_total,
+ cost=span_db.cost,
+ tags=span_db.tags,
+ ).dict(exclude_unset=True)
+ for span_db in spans_db
+ ]
+
+
+def feedback_db_to_pydantic(feedback_db: FeedbackDB) -> FeedbackOutput:
+ return FeedbackOutput(
+ feedback_id=str(feedback_db.uid),
+ feedback=feedback_db.feedback,
+ score=feedback_db.score,
+ meta=feedback_db.meta,
+ created_at=feedback_db.created_at,
+ ).dict(exclude_unset=True)
+
+
+def trace_db_to_pydantic(trace_db: TraceDB) -> Trace:
+ feedbacks = trace_db.feedbacks
+ if feedbacks is None:
+ result = []
+ else:
+ result = [
+ feedback_db_to_pydantic(feedback)
+ for feedback in feedbacks
+ if feedback is not None
+ ]
+
+ return Trace(
+ trace_id=str(trace_db.id),
+ app_id=trace_db.app_id,
+ variant_id=trace_db.variant_id,
+ cost=trace_db.cost,
+ latency=trace_db.latency,
+ status=trace_db.status,
+ token_consumption=trace_db.token_consumption,
+ tags=trace_db.tags,
+ start_time=trace_db.start_time,
+ end_time=trace_db.end_time,
+ feedbacks=result,
+ spans=[str(span) for span in trace_db.spans],
+ ).dict(exclude_unset=True)
+
+
+def user_db_to_pydantic(user_db: UserDB) -> User:
+ return User(
+ id=str(user_db.id),
+ uid=user_db.uid,
+ username=user_db.username,
+ email=user_db.email,
+ ).dict(exclude_unset=True)
diff --git a/agenta-backend/agenta_backend/models/db_engine.py b/agenta-backend/agenta_backend/models/db_engine.py
new file mode 100644
index 0000000000..6bb56359e6
--- /dev/null
+++ b/agenta-backend/agenta_backend/models/db_engine.py
@@ -0,0 +1,74 @@
+import os
+import logging
+
+from odmantic import AIOEngine
+from pymongo import MongoClient
+from motor.motor_asyncio import AsyncIOMotorClient
+
+
+# Configure and set logging level
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+
+class DBEngine(object):
+ """
+ Database engine to initialize client and return engine based on mode
+ """
+
+ def __init__(self) -> None:
+ self.mode = os.environ.get("DATABASE_MODE", "v2")
+ self.db_url = os.environ["MONGODB_URI"]
+
+ @property
+ def initialize_client(self) -> AsyncIOMotorClient:
+ """
+ Returns an instance of `AsyncIOMotorClient` initialized \
+ with the provided `db_url`.
+ """
+
+ client = AsyncIOMotorClient(self.db_url)
+ return client
+
+ def engine(self) -> AIOEngine:
+ """
+ Returns an AIOEngine object with a specified database name based on the mode.
+ """
+
+ if self.mode == "test":
+ aio_engine = AIOEngine(
+ client=self.initialize_client, database="agenta_test"
+ )
+ logger.info("Using test database...")
+ return aio_engine
+ elif self.mode == "default":
+ aio_engine = AIOEngine(client=self.initialize_client, database="agenta")
+ logger.info("Using default database...")
+ return aio_engine
+ elif self.mode == "v2":
+ aio_engine = AIOEngine(client=self.initialize_client, database="agenta_v2")
+ logger.info("Using v2 database...")
+ return aio_engine
+ else:
+ # make sure that self.mode does only contain alphanumeric characters
+ if not self.mode.isalnum():
+ raise ValueError("Mode of database needs to be alphanumeric.")
+ aio_engine = AIOEngine(
+ client=self.initialize_client, database=f"agenta_{self.mode}"
+ )
+ logger.info(f"Using {self.mode} database...")
+ return aio_engine
+ raise ValueError(
+ "Mode of database is unknown. Did you mean 'default' or 'test'?"
+ )
+
+ def remove_db(self) -> None:
+ """
+ Remove the database based on the mode.
+ """
+
+ client = MongoClient(self.db_url)
+ if self.mode == "default":
+ client.drop_database("agenta")
+ elif self.mode == "test":
+ client.drop_database("agenta_test")
diff --git a/agenta-backend/agenta_backend/models/db_models.py b/agenta-backend/agenta_backend/models/db_models.py
index a888e1f04c..6a243e57b4 100644
--- a/agenta-backend/agenta_backend/models/db_models.py
+++ b/agenta-backend/agenta_backend/models/db_models.py
@@ -1,38 +1,310 @@
from datetime import datetime
-from typing import List, Optional, Dict
-from sqlmodel import SQLModel, Field, JSON, Column
+from typing import Any, Dict, List, Optional, Union
+from uuid import uuid4
+from bson import ObjectId
+from odmantic import EmbeddedModel, Field, Model, Reference
-class ImageDB(SQLModel, table=True):
+
+class APIKeyDB(Model):
+ prefix: str
+ hashed_key: str
+ user_id: str
+ rate_limit: int = Field(default=0)
+ hidden: Optional[bool] = Field(default=False)
+ expiration_date: Optional[datetime]
+ created_at: Optional[datetime] = datetime.utcnow()
+ updated_at: Optional[datetime]
+
+ class Config:
+ collection = "api_keys"
+
+
+class InvitationDB(EmbeddedModel):
+ token: str = Field(unique=True)
+ email: str
+ expiration_date: datetime = Field(default="0")
+ used: bool = False
+
+
+class OrganizationDB(Model):
+ name: str = Field(default="agenta")
+ description: str = Field(default="")
+ type: Optional[str]
+ owner: str # user id
+ members: Optional[List[ObjectId]]
+ invitations: Optional[List[InvitationDB]] = []
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "organizations"
+
+
+class UserDB(Model):
+ uid: str = Field(default="0", unique=True, index=True)
+ username: str = Field(default="agenta")
+ email: str = Field(default="demo@agenta.ai", unique=True)
+ organizations: Optional[List[ObjectId]] = []
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "users"
+
+
+class ImageDB(Model):
"""Defines the info needed to get an image and connect it to the app variant"""
- id: int = Field(default=None, primary_key=True)
- docker_id: str = Field(...)
- tags: str = Field(...)
+ type: Optional[str] = Field(default="image")
+ template_uri: Optional[str]
+ docker_id: Optional[str] = Field(index=True)
+ tags: Optional[str]
+ deletable: bool = Field(default=True)
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+ deletable: bool = Field(default=True)
+
+ class Config:
+ collection = "docker_images"
+
+
+class AppDB(Model):
+ app_name: str
+ organization: OrganizationDB = Reference(key_name="organization")
+ user: UserDB = Reference(key_name="user")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
-class AppVariantDB(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
- app_name: str = Field(...)
- variant_name: str = Field(...)
- image_id: int = Field(foreign_key="imagedb.id")
- parameters: Dict = Field(sa_column=Column(JSON))
- previous_variant_name: Optional[str] = Field(default=None)
- is_deleted: bool = Field(
+class DeploymentDB(Model):
+ app: AppDB = Reference(key_name="app")
+ organization: OrganizationDB = Reference(key_name="organization")
+ user: UserDB = Reference(key_name="user")
+ container_name: Optional[str]
+ container_id: Optional[str]
+ uri: Optional[str]
+ status: str
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "deployments"
+
+
+class VariantBaseDB(Model):
+ app: AppDB = Reference(key_name="app")
+ organization: OrganizationDB = Reference(key_name="organization")
+ user: UserDB = Reference(key_name="user")
+ base_name: str
+ image: ImageDB = Reference(key_name="image")
+ deployment: Optional[ObjectId] # Reference to deployment
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "bases"
+
+
+class ConfigVersionDB(EmbeddedModel):
+ version: int
+ parameters: Dict[str, Any]
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+
+class ConfigDB(Model):
+ config_name: str
+ current_version: int = Field(default=1)
+ parameters: Dict[str, Any] = Field(default=dict)
+ version_history: List[ConfigVersionDB] = Field(default=[])
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "configs"
+
+
+class AppVariantDB(Model):
+ app: AppDB = Reference(key_name="app")
+ variant_name: str
+ image: ImageDB = Reference(key_name="image")
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ parameters: Dict[str, Any] = Field(default=dict) # TODO: deprecated. remove
+ previous_variant_name: Optional[str] # TODO: deprecated. remove
+ base_name: Optional[str]
+ base: VariantBaseDB = Reference(key_name="bases")
+ config_name: Optional[str]
+ config: ConfigDB = Reference(key_name="configs")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ is_deleted: bool = Field( # TODO: deprecated. remove
default=False
) # soft deletion for using the template variants
+ class Config:
+ collection = "app_variants"
+
+
+class AppEnvironmentDB(Model):
+ app: AppDB = Reference(key_name="app")
+ name: str
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ deployed_app_variant: Optional[ObjectId]
+ deployment: Optional[ObjectId] # reference to deployment
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "environments"
+
+
+class TemplateDB(Model):
+ type: Optional[str] = Field(default="image")
+ template_uri: Optional[str]
+ tag_id: Optional[int]
+ name: str = Field(unique=True) # tag name of image
+ repo_name: Optional[str]
+ title: str
+ description: str
+ size: Optional[int]
+ digest: Optional[str] # sha256 hash of image digest
+ last_pushed: Optional[datetime]
+
+ class Config:
+ collection = "templates"
+
+
+class TestSetDB(Model):
+ name: str
+ app: AppDB = Reference(key_name="app")
+ csvdata: List[Dict[str, str]]
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "testsets"
+
+
+class EvaluationTypeSettings(EmbeddedModel):
+ similarity_threshold: Optional[float]
+ regex_pattern: Optional[str]
+ regex_should_match: Optional[bool]
+ webhook_url: Optional[str]
+ llm_app_prompt_template: Optional[str]
+ custom_code_evaluation_id: Optional[str]
+ evaluation_prompt_template: Optional[str]
+
+
+class EvaluationScenarioInput(EmbeddedModel):
+ input_name: str
+ input_value: str
+
+
+class EvaluationScenarioOutput(EmbeddedModel):
+ variant_id: str
+ variant_output: str
+
+
+class EvaluationDB(Model):
+ app: AppDB = Reference(key_name="app")
+ organization: OrganizationDB = Reference(key_name="organization")
+ user: UserDB = Reference(key_name="user")
+ status: str
+ evaluation_type: str
+ evaluation_type_settings: EvaluationTypeSettings
+ variants: List[ObjectId]
+ testset: TestSetDB = Reference(key_name="testsets")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "evaluations"
+
+
+class EvaluationScenarioDB(Model):
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ evaluation: EvaluationDB = Reference(key_name="evaluations")
+ inputs: List[EvaluationScenarioInput]
+ outputs: List[EvaluationScenarioOutput] # EvaluationScenarioOutput
+ vote: Optional[str]
+ score: Optional[Union[str, int]]
+ correct_answer: Optional[str]
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+ is_pinned: Optional[bool]
+ note: Optional[str]
+
+ class Config:
+ collection = "evaluation_scenarios"
+
+
+class CustomEvaluationDB(Model):
+ evaluation_name: str
+ python_code: str
+ app: AppDB = Reference(key_name="app")
+ user: UserDB = Reference(key_name="user")
+ organization: OrganizationDB = Reference(key_name="organization")
+ created_at: Optional[datetime] = Field(default=datetime.utcnow())
+ updated_at: Optional[datetime] = Field(default=datetime.utcnow())
+
+ class Config:
+ collection = "custom_evaluations"
+
+
+class SpanDB(Model):
+ parent_span_id: Optional[str]
+ meta: Optional[Dict[str, Any]]
+ event_name: str # Function or execution name
+ event_type: Optional[str]
+ start_time: datetime
+ duration: Optional[int]
+ status: str # initiated, completed, stopped, cancelled
+ end_time: datetime = Field(default=datetime.utcnow())
+ inputs: Optional[List[str]]
+ outputs: Optional[List[str]]
+ prompt_template: Optional[str]
+ tokens_input: Optional[int]
+ tokens_output: Optional[int]
+ token_total: Optional[int]
+ cost: Optional[float]
+ tags: Optional[List[str]]
+
+ class Config:
+ collection = "spans"
+
+
+class Feedback(EmbeddedModel):
+ uid: str = Field(default=str(uuid4()))
+ user_id: str
+ feedback: Optional[str]
+ score: Optional[float]
+ meta: Optional[Dict[str, Any]]
+ created_at: datetime
+ updated_at: datetime = Field(default=datetime.utcnow())
+
+
+class TraceDB(Model):
+ app_id: Optional[str]
+ variant_id: str
+ spans: List[ObjectId]
+ start_time: datetime
+ end_time: datetime = Field(default=datetime.utcnow())
+ cost: Optional[float]
+ latency: float
+ status: str # initiated, completed, stopped, cancelled, failed
+ token_consumption: Optional[int]
+ user: UserDB = Reference()
+ tags: Optional[List[str]]
+ feedbacks: Optional[List[Feedback]]
-class TemplateDB(SQLModel, table=True):
- id: Optional[int] = Field(default=None, primary_key=True)
- template_id: int = Field(...)
- name: str = Field(...)
- repo_name: str = Field(...)
- architecture: str = Field(...)
- title: str = Field(...)
- description: str = Field(...)
- size: int = Field(...)
- digest: str = Field(...)
- status: str = Field(...)
- media_type: str = Field()
- last_pushed: datetime = Field(...)
+ class Config:
+ collection = "traces"
diff --git a/agenta-backend/agenta_backend/pytest.ini b/agenta-backend/agenta_backend/pytest.ini
new file mode 100644
index 0000000000..b4a0b1ec70
--- /dev/null
+++ b/agenta-backend/agenta_backend/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+DATABASE_MODE=test
\ No newline at end of file
diff --git a/agenta-backend/agenta_backend/resources/default_testsets/single_prompt_testsets.json b/agenta-backend/agenta_backend/resources/default_testsets/single_prompt_testsets.json
new file mode 100644
index 0000000000..eb5a2120dc
--- /dev/null
+++ b/agenta-backend/agenta_backend/resources/default_testsets/single_prompt_testsets.json
@@ -0,0 +1,30 @@
+[
+ {
+ "country": "Nauru",
+ "correct_answer": "The capital of Nauru is Funafuti."
+ },
+ {
+ "country": "Tuvalu",
+ "correct_answer": "The capital of Tuvalu is Funafuti."
+ },
+ {
+ "country": "Brunei",
+ "correct_answer": "The capital of Brunei is Bandar Seri Begawan."
+ },
+ {
+ "country": "Kiribati",
+ "correct_answer": "The capital of Kiribati is Tarawa."
+ },
+ {
+ "country": "Comoros",
+ "correct_answer": "The capital of Comoros is Moroni."
+ },
+ {
+ "country": "Kyrgyzstan",
+ "correct_answer": "The capital of Kyrgyzstan is Bishkek."
+ },
+ {
+ "country": "Azerbaijan",
+ "correct_answer": "The capital of Azerbaijan is Baku."
+ }
+]
\ No newline at end of file
diff --git a/agenta-backend/agenta_backend/routers/app_router.py b/agenta-backend/agenta_backend/routers/app_router.py
new file mode 100644
index 0000000000..0ff57c034b
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/app_router.py
@@ -0,0 +1,453 @@
+import os
+import logging
+from docker.errors import DockerException
+from fastapi.responses import JSONResponse
+from agenta_backend.config import settings
+from typing import List, Optional
+from fastapi import APIRouter, HTTPException, Request
+from agenta_backend.services.selectors import get_user_own_org
+from agenta_backend.services import (
+ app_manager,
+ db_manager,
+)
+from agenta_backend.utils.common import (
+ check_access_to_app,
+ check_user_org_access,
+)
+from agenta_backend.models.api.api_models import (
+ App,
+ CreateApp,
+ CreateAppOutput,
+ CreateAppVariant,
+ AppVariantOutput,
+ AddVariantFromImagePayload,
+ EnvironmentOutput,
+ Image,
+)
+from agenta_backend.models import converters
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+if os.environ["FEATURE_FLAG"] in ["cloud"]:
+ from agenta_backend.cloud.services import (
+ lambda_deployment_manager as deployment_manager,
+ ) # noqa pylint: disable-all
+elif os.environ["FEATURE_FLAG"] in ["ee"]:
+ from agenta_backend.ee.services import (
+ deployment_manager,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services import deployment_manager
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+@router.get("/{app_id}/variants/", response_model=List[AppVariantOutput])
+async def list_app_variants(
+ app_id: str,
+ request: Request,
+):
+ """
+ Retrieve a list of app variants for a given app ID.
+
+ Args:
+ app_id (str): The ID of the app to retrieve variants for.
+ stoken_session (SessionContainer, optional): The session container to verify the user's session. Defaults to Depends(verify_session()).
+
+ Returns:
+ List[AppVariantOutput]: A list of app variants for the given app ID.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id
+ )
+ if not access_app:
+ error_msg = f"You cannot access app: {app_id}"
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=403,
+ )
+
+ app_variants = await db_manager.list_app_variants(
+ app_id=app_id, **user_org_data
+ )
+ return [
+ await converters.app_variant_db_to_output(app_variant)
+ for app_variant in app_variants
+ ]
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/get_variant_by_env/", response_model=AppVariantOutput)
+async def get_variant_by_env(
+ app_id: str,
+ environment: str,
+ request: Request,
+):
+ """
+ Retrieve the app variant based on the provided app_id and environment.
+
+ Args:
+ app_id (str): The ID of the app to retrieve the variant for.
+ environment (str): The environment of the app variant to retrieve.
+ stoken_session (SessionContainer, optional): The session token container. Defaults to Depends(verify_session()).
+
+ Raises:
+ HTTPException: If the app variant is not found (status_code=500), or if a ValueError is raised (status_code=400), or if any other exception is raised (status_code=500).
+
+ Returns:
+ AppVariantOutput: The retrieved app variant.
+ """
+ try:
+ # Retrieve the user and organization ID based on the session token
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ await check_access_to_app(user_org_data, app_id=app_id)
+
+ # Fetch the app variant using the provided app_id and environment
+ app_variant_db = await db_manager.get_app_variant_by_app_name_and_environment(
+ app_id=app_id, environment=environment, **user_org_data
+ )
+
+ # Check if the fetched app variant is None and raise exception if it is
+ if app_variant_db is None:
+ raise HTTPException(status_code=500, detail="App Variant not found")
+ return await converters.app_variant_db_to_output(app_variant_db)
+ except ValueError as e:
+ # Handle ValueErrors and return 400 status code
+ raise HTTPException(status_code=400, detail=str(e))
+ except HTTPException as e:
+ raise e
+ except Exception as e:
+ # Handle all other exceptions and return 500 status code
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/", response_model=CreateAppOutput)
+async def create_app(
+ payload: CreateApp,
+ request: Request,
+) -> CreateAppOutput:
+ """
+ Create a new app for a user or organization.
+
+ Args:
+ payload (CreateApp): The payload containing the app name and organization ID (optional).
+ stoken_session (SessionContainer): The session container containing the user's session token.
+
+ Returns:
+ CreateAppOutput: The output containing the newly created app's ID and name.
+
+ Raises:
+ HTTPException: If there is an error creating the app or the user does not have permission to access the app.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ if payload.organization_id:
+ access = await check_user_org_access(user_org_data, payload.organization_id)
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail="You do not have permission to access this app",
+ )
+ organization_id = payload.organization_id
+ else:
+ # Retrieve or create user organization
+ organization = await get_user_own_org(user_org_data["uid"])
+ if organization is None: # TODO: Check whether we need this
+ logger.error("Organization for user not found.")
+ organization = await db_manager.create_user_organization(
+ user_org_data["uid"]
+ )
+ organization_id = str(organization.id)
+
+ app_db = await db_manager.create_app_and_envs(
+ payload.app_name, organization_id, **user_org_data
+ )
+ return CreateAppOutput(app_id=str(app_db.id), app_name=str(app_db.app_name))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/", response_model=List[App])
+async def list_apps(
+ request: Request,
+ app_name: Optional[str] = None,
+ org_id: Optional[str] = None,
+) -> List[App]:
+ """
+ Retrieve a list of apps filtered by app_name and org_id.
+
+ Args:
+ app_name (Optional[str]): The name of the app to filter by.
+ org_id (Optional[str]): The ID of the organization to filter by.
+ stoken_session (SessionContainer): The session container.
+
+ Returns:
+ List[App]: A list of apps filtered by app_name and org_id.
+
+ Raises:
+ HTTPException: If there was an error retrieving the list of apps.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ apps = await db_manager.list_apps(app_name, org_id, **user_org_data)
+ return apps
+ except Exception as e:
+ logger.error(f"list_apps exception ===> {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/{app_id}/variant/from-image/")
+async def add_variant_from_image(
+ app_id: str,
+ payload: AddVariantFromImagePayload,
+ request: Request,
+):
+ """
+ Add a new variant to an app based on a Docker image.
+
+ Args:
+ app_id (str): The ID of the app to add the variant to.
+ payload (AddVariantFromImagePayload): The payload containing information about the variant to add.
+ stoken_session (SessionContainer, optional): The session container. Defaults to Depends(verify_session()).
+
+ Raises:
+ HTTPException: If the feature flag is set to "demo" or if the image does not have a tag starting with the registry name (agenta-server) or if the image is not found or if the user does not have access to the app.
+
+ Returns:
+ dict: The newly added variant.
+ """
+
+ if os.environ["FEATURE_FLAG"] not in ["cloud", "ee"]:
+ image = Image(
+ type="image",
+ docker_id=payload.docker_id,
+ tags=payload.tags,
+ )
+ if not payload.tags.startswith(settings.registry):
+ raise HTTPException(
+ status_code=500,
+ detail="Image should have a tag starting with the registry name (agenta-server)",
+ )
+ elif await deployment_manager.validate_image(image) is False:
+ raise HTTPException(status_code=404, detail="Image not found")
+
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(user_org_data, app_id=app_id)
+ if not access_app:
+ error_msg = f"You cannot access app: {app_id}"
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=403,
+ )
+ app = await db_manager.fetch_app_by_id(app_id)
+
+ app_variant_db = await app_manager.add_variant_based_on_image(
+ app=app,
+ variant_name=payload.variant_name,
+ docker_id_or_template_uri=payload.docker_id,
+ tags=payload.tags,
+ base_name=payload.base_name,
+ config_name=payload.config_name,
+ is_template_image=False,
+ **user_org_data,
+ )
+ return await converters.app_variant_db_to_output(app_variant_db)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.delete("/{app_id}/")
+async def remove_app(app_id: str, request: Request):
+ """Remove app, all its variant, containers and images
+
+ Arguments:
+ app -- App to remove
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data, app_id=app_id, check_owner=True
+ )
+
+ if not access_app:
+ error_msg = f"You do not have permission to delete app: {app_id}"
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ else:
+ await app_manager.remove_app(app_id=app_id, **user_org_data)
+ except DockerException as e:
+ detail = f"Docker error while trying to remove the app: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except Exception as e:
+ detail = f"Unexpected error while trying to remove the app: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+
+
+@router.post("/app_and_variant_from_template/")
+async def create_app_and_variant_from_template(
+ payload: CreateAppVariant,
+ request: Request,
+) -> AppVariantOutput:
+ """
+ Create an app and variant from a template.
+
+ Args:
+ payload (CreateAppVariant): The payload containing the app and variant information.
+ stoken_session (SessionContainer, optional): The session container. Defaults to Depends(verify_session()).
+
+ Raises:
+ HTTPException: If the user has reached the app limit or if an app with the same name already exists.
+
+ Returns:
+ AppVariantOutput: The output of the created app variant.
+ """
+ try:
+ logger.debug("Start: Creating app and variant from template")
+
+ # Get user and org id
+ logger.debug("Step 1: Getting user and organization ID")
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ logger.debug("Step 2: Setting organization ID")
+ if payload.organization_id is None:
+ organization = await get_user_own_org(user_org_data["uid"])
+ organization_id = organization.id
+ else:
+ organization_id = payload.organization_id
+
+ logger.debug(f"Step 3 Checking if app {payload.app_name} already exists")
+ app_name = payload.app_name.lower()
+ app = await db_manager.fetch_app_by_name_and_organization(
+ app_name, organization_id, **user_org_data
+ )
+ if app is not None:
+ raise HTTPException(
+ status_code=400,
+ detail=f"App with name {app_name} already exists",
+ )
+
+ logger.debug("Step 4: Creating new app and initializing environments")
+ if app is None:
+ app = await db_manager.create_app_and_envs(
+ app_name, organization_id, **user_org_data
+ )
+
+ logger.debug("Step 5: Retrieve template from db")
+ template_db = await db_manager.get_template(payload.template_id)
+ repo_name = os.environ.get("AGENTA_TEMPLATE_REPO", "agentaai/lambda_templates")
+ image_name = f"{repo_name}:{template_db.name}"
+
+ logger.debug(
+ "Step 6: Creating image instance and adding variant based on image"
+ )
+ app_variant_db = await app_manager.add_variant_based_on_image(
+ app=app,
+ variant_name="app.default",
+ docker_id_or_template_uri=template_db.template_uri
+ if os.environ["FEATURE_FLAG"] in ["cloud"]
+ else template_db.digest,
+ tags=f"{image_name}"
+ if os.environ["FEATURE_FLAG"] not in ["cloud"]
+ else None,
+ base_name="app",
+ config_name="default",
+ is_template_image=True,
+ **user_org_data,
+ )
+
+ logger.debug("Step 7: Creating testset for app variant")
+ await db_manager.add_testset_to_app_variant(
+ app_id=str(app.id),
+ org_id=organization_id,
+ template_name=template_db.name,
+ app_name=app.app_name,
+ **user_org_data,
+ )
+
+ logger.debug("Step 8: Starting variant and injecting environment variables")
+ if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ if not os.environ["OPENAI_API_KEY"]:
+ raise HTTPException(
+ status_code=400,
+ detail="Unable to start app container. Please file an issue by clicking on the button below.",
+ )
+ envvars = {
+ **(payload.env_vars or {}),
+ "OPENAI_API_KEY": os.environ[
+ "OPENAI_API_KEY"
+ ], # order is important here
+ }
+ else:
+ envvars = {} if payload.env_vars is None else payload.env_vars
+
+ await app_manager.start_variant(app_variant_db, envvars, **user_org_data)
+
+ logger.debug("End: Successfully created app and variant")
+ return await converters.app_variant_db_to_output(app_variant_db)
+
+ except Exception as e:
+ logger.debug(f"Error: Exception caught - {str(e)}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/{app_id}/environments/", response_model=List[EnvironmentOutput])
+async def list_environments(
+ app_id: str,
+ request: Request,
+):
+ """
+ Retrieve a list of environments for a given app ID.
+
+ Args:
+ app_id (str): The ID of the app to retrieve environments for.
+ stoken_session (SessionContainer, optional): The session container. Defaults to Depends(verify_session()).
+
+ Returns:
+ List[EnvironmentOutput]: A list of environment objects.
+ """
+ logger.debug(f"Listing environments for app: {app_id}")
+ try:
+ logger.debug("get user and org data")
+ user_and_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Check if has app access
+ logger.debug("check_access_to_app")
+ access_app = await check_access_to_app(
+ user_org_data=user_and_org_data, app_id=app_id
+ )
+ logger.debug(f"access_app: {access_app}")
+ if not access_app:
+ error_msg = f"You do not have access to this app: {app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ else:
+ environments_db = await db_manager.list_environments(
+ app_id=app_id, **user_and_org_data
+ )
+ logger.debug(f"environments_db: {environments_db}")
+ return [
+ await converters.environment_db_to_output(env)
+ for env in environments_db
+ ]
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/agenta-backend/agenta_backend/routers/app_variant.py b/agenta-backend/agenta_backend/routers/app_variant.py
deleted file mode 100644
index 17dbf9c791..0000000000
--- a/agenta-backend/agenta_backend/routers/app_variant.py
+++ /dev/null
@@ -1,280 +0,0 @@
-"""Routes for image-related operations (push, remove).
-Does not deal with the instanciation of the images
-"""
-import logging
-from typing import Any, Dict, List, Optional
-
-from agenta_backend.config import settings
-from agenta_backend.models.api.api_models import (
- URI,
- App,
- AppVariant,
- Image,
- DockerEnvVars,
- CreateAppVariant,
-)
-from agenta_backend.services import app_manager, db_manager, docker_utils
-from docker.errors import DockerException
-from fastapi import APIRouter, Body, HTTPException
-from sqlalchemy.exc import SQLAlchemyError
-
-router = APIRouter()
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-
-
-@router.get("/list_variants/", response_model=List[AppVariant])
-async def list_app_variants(app_name: Optional[str] = None):
- """Lists the app variants from our repository.
-
- Arguments:
- app_name -- If specified, only returns the app variants for the specified app
- Raises:
- HTTPException: _description_
-
- Returns:
- List[AppVariant]
- """
- try:
- app_variants = db_manager.list_app_variants(app_name=app_name)
- return app_variants
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.get("/list_apps/", response_model=List[App])
-async def list_apps() -> List[App]:
- """Lists the apps from our repository.
-
- Raises:
- HTTPException: _description_
-
- Returns:
- List[App]
- """
- try:
- apps = db_manager.list_apps()
- return apps
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/add/from_image/")
-async def add_variant_from_image(app_variant: AppVariant, image: Image):
- """Add a variant to the server based on an image.
-
- Arguments:
- app_variant -- AppVariant to add
- image -- The image tags should start with the registry name (agenta-server) and end with :latest
-
- Raises:
- HTTPException: If image tag doesn't start with registry name
- HTTPException: If image not found in docker utils list
- HTTPException: If there is a problem adding the app variant
- """
-
- if not image.tags.startswith(settings.registry):
- raise HTTPException(
- status_code=500,
- detail="Image should have a tag starting with the registry name (agenta-server)",
- )
- elif image not in docker_utils.list_images():
- raise HTTPException(status_code=500, detail="Image not found")
-
- try:
- db_manager.add_variant_based_on_image(app_variant, image)
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/add/from_previous/")
-async def add_variant_from_previous(
- previous_app_variant: AppVariant,
- new_variant_name: str = Body(...),
- parameters: Dict[str, Any] = Body(...),
-):
- """Add a variant to the server based on a previous variant.
-
- Arguments:
- app_variant -- AppVariant to add
- previous_app_variant -- Previous AppVariant to use as a base
- parameters -- parameters for the variant
-
- Raises:
- HTTPException: If there is a problem adding the app variant
- """
- print(
- f"previous_app_variant: {previous_app_variant}, type: {type(previous_app_variant)}"
- )
- print(f"new_variant_name: {new_variant_name}, type: {type(new_variant_name)}")
- print(f"parameters: {parameters}, type: {type(parameters)}")
- try:
- db_manager.add_variant_based_on_previous(
- previous_app_variant, new_variant_name, parameters
- )
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/start/")
-async def start_variant(
- app_variant: AppVariant, env_vars: Optional[DockerEnvVars] = None
-) -> URI:
- print(f"Starting variant {app_variant}")
- logger.info("Starting variant %s", app_variant)
- try:
- env_vars = {} if env_vars is None else env_vars.env_vars
- return app_manager.start_variant(app_variant, env_vars)
- except Exception as e:
- if db_manager.get_variant_from_db(app_variant) is not None:
- app_manager.remove_app_variant(app_variant)
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/stop/")
-async def stop_variant(app_variant: AppVariant):
- assert NotImplementedError("Not implemented yet")
-
-
-@router.get("/list_images/", response_model=List[Image])
-async def list_images():
- """Lists the images from our repository
-
- Raises:
- HTTPException: _description_
-
- Returns:
- List[AppVariant]
- """
- try:
- list_images = docker_utils.list_images()
- return list_images
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.delete("/remove_variant/")
-async def remove_variant(app_variant: AppVariant):
- """Remove a variant from the server.
- In the case it's the last variant using the image, stop the container and remove the image.
-
- Arguments:
- app_variant -- AppVariant to remove
-
- Raises:
- HTTPException: If there is a problem removing the app variant
- """
- try:
- app_manager.remove_app_variant(app_variant)
- except SQLAlchemyError as e:
- detail = f"Database error while trying to remove the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except DockerException as e:
- detail = f"Docker error while trying to remove the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except Exception as e:
- detail = f"Unexpected error while trying to remove the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
-
-
-@router.delete("/remove_app/")
-async def remove_app(app: App):
- """Remove app, all its variant, containers and images
-
- Arguments:
- app -- App to remove
- """
- try:
- await app_manager.remove_app(app)
- except SQLAlchemyError as e:
- detail = f"Database error while trying to remove the app: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except DockerException as e:
- detail = f"Docker error while trying to remove the app: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except Exception as e:
- detail = f"Unexpected error while trying to remove the app: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
-
-
-@router.put("/update_variant_parameters/")
-async def update_variant_parameters(app_variant: AppVariant):
- """Updates the parameters for an app variant
-
- Arguments:
- app_variant -- Appvariant to update
- """
- try:
- app_manager.update_variant_parameters(app_variant)
- except ValueError as e:
- detail = f"Error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except SQLAlchemyError as e:
- detail = f"Database error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
- except Exception as e:
- detail = f"Unexpected error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=detail)
-
-
-@router.put("/update_variant_image/")
-async def update_variant_image(app_variant: AppVariant, image: Image):
- """Updates the image used in an app variant
-
- Arguments:
- app_variant -- the app variant to update
- image -- the image information
- """
- try:
- app_manager.update_variant_image(app_variant, image)
- except ValueError as e:
- detail = f"Error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=str(e))
- except SQLAlchemyError as e:
- detail = f"Database error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=str(e))
- except DockerException as e:
- detail = f"Docker error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=str(e))
- except Exception as e:
- detail = f"Unexpected error while trying to update the app variant: {str(e)}"
- raise HTTPException(status_code=500, detail=str(e))
-
-
-@router.post("/add/from_template/")
-async def add_app_variant_from_template(payload: CreateAppVariant):
- """Creates or updates an app variant based on the provided image and starts the variant
-
- Arguments:
- payload -- a data model that contains the necessary information to create an app variant from an image
-
- Returns:
- a JSON response with a message and data
- """
-
- # Create an AppVariant with the provided app name
- app_variant: AppVariant = AppVariant(app_name=payload.app_name, variant_name="v1")
-
- # Create an Image instance with the extracted image id, and defined image name
- image_id = payload.image_id.split(":")[-1]
- image_name = f"agentaai/templates:{payload.image_tag}"
- image: Image = Image(docker_id=image_id, tags=f"{image_name}")
-
- variant_exist = db_manager.get_variant_from_db(app_variant)
- if variant_exist is None:
- # Save variant based on the image to database
- db_manager.add_variant_based_on_image(app_variant, image)
- else:
- # Update variant based on the image
- app_manager.update_variant_image(app_variant, image)
-
- # Start variant
- url = app_manager.start_variant(app_variant, payload.env_vars)
-
- return {
- "message": "Variant created and running!",
- "data": {
- "url": url.uri,
- "playground": f"http://localhost:3000/apps/{payload.app_name}/playground",
- },
- }
diff --git a/agenta-backend/agenta_backend/routers/bases_router.py b/agenta-backend/agenta_backend/routers/bases_router.py
new file mode 100644
index 0000000000..bda4d348fb
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/bases_router.py
@@ -0,0 +1,63 @@
+import os
+from typing import List, Optional
+from fastapi import APIRouter, Request, HTTPException
+from agenta_backend.models.api.api_models import BaseOutput
+from fastapi.responses import JSONResponse
+from agenta_backend.services import db_manager
+from agenta_backend.models import converters
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+from agenta_backend.utils.common import check_access_to_app
+
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[BaseOutput])
+async def list_bases(
+ request: Request,
+ app_id: Optional[str] = None,
+ base_name: Optional[str] = None,
+) -> List[BaseOutput]:
+ """
+ Retrieve a list of bases filtered by app_id and base_name.
+
+ Args:
+ request (Request): The incoming request.
+ app_id (Optional[str], optional): The ID of the app to filter by. Defaults to None.
+ base_name (Optional[str], optional): The name of the base to filter by. Defaults to None.
+
+ Returns:
+ List[BaseOutput]: A list of BaseOutput objects representing the filtered bases.
+
+ Raises:
+ HTTPException: If there was an error retrieving the bases.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id
+ )
+ if not access_app:
+ error_msg = f"You cannot access app: {app_id}"
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=403,
+ )
+ bases = await db_manager.list_bases_for_app_id(
+ app_id, base_name, **user_org_data
+ )
+ return [converters.base_db_to_pydantic(base) for base in bases]
+ except Exception as e:
+ logger.error(f"list_bases exception ===> {e}")
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/agenta-backend/agenta_backend/routers/configs_router.py b/agenta-backend/agenta_backend/routers/configs_router.py
new file mode 100644
index 0000000000..d0a09ac5e5
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/configs_router.py
@@ -0,0 +1,137 @@
+import os
+from typing import Optional
+from fastapi import APIRouter, Request, HTTPException
+import logging
+
+from agenta_backend.models.api.api_models import (
+ SaveConfigPayload,
+ GetConfigReponse,
+)
+from agenta_backend.services import (
+ db_manager,
+ app_manager,
+)
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+router = APIRouter()
+
+
+@router.post("/")
+async def save_config(
+ payload: SaveConfigPayload,
+ request: Request,
+):
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ base_db = await db_manager.fetch_base_and_check_access(
+ payload.base_id, user_org_data
+ )
+ variants_db = await db_manager.list_variants_for_base(base_db, **user_org_data)
+ variant_to_overwrite = None
+ for variant_db in variants_db:
+ if variant_db.config_name == payload.config_name:
+ variant_to_overwrite = variant_db
+ break
+ if variant_to_overwrite is not None:
+ if payload.overwrite:
+ print(f"update_variant_parameters ===> {payload.overwrite}")
+ await app_manager.update_variant_parameters(
+ app_variant_id=str(variant_to_overwrite.id),
+ parameters=payload.parameters,
+ **user_org_data,
+ )
+ else:
+ raise HTTPException(
+ status_code=400,
+ detail="Config name already exists. Please use a different name or set overwrite to True.",
+ )
+ else:
+ print(
+ f"add_variant_from_base_and_config overwrite ===> {payload.overwrite}"
+ )
+ await db_manager.add_variant_from_base_and_config(
+ base_db=base_db,
+ new_config_name=payload.config_name,
+ parameters=payload.parameters,
+ **user_org_data,
+ )
+ except HTTPException as e:
+ logger.error(f"save_config http exception ===> {e.detail}")
+ raise
+ except Exception as e:
+ logger.error(f"save_config exception ===> {e}")
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+@router.get("/", response_model=GetConfigReponse)
+async def get_config(
+ request: Request,
+ base_id: str,
+ config_name: Optional[str] = None,
+ environment_name: Optional[str] = None,
+):
+ try:
+ # detemine whether the user has access to the base
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ base_db = await db_manager.fetch_base_and_check_access(base_id, user_org_data)
+ # in case environment_name is provided, find the variant deployed
+ if environment_name:
+ app_environments = await db_manager.list_environments(
+ app_id=str(base_db.app.id)
+ )
+ found_variant = None
+ for app_environments in app_environments:
+ if app_environments.name == environment_name:
+ found_variant = await db_manager.get_app_variant_instance_by_id(
+ str(app_environments.deployed_app_variant)
+ )
+ break
+ if not found_variant:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Environment name {environment_name} not found for base {base_id}",
+ )
+ if str(found_variant.base.id) != base_id:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Environment {environment_name} does not deploy base {base_id}",
+ )
+ config = found_variant.config
+ elif config_name:
+ variants_db = await db_manager.list_variants_for_base(
+ base_db, **user_org_data
+ )
+ found_variant = None
+ for variant_db in variants_db:
+ if variant_db.config_name == config_name:
+ found_variant = variant_db
+ break
+ if not found_variant:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Config name {config_name} not found for base {base_id}",
+ )
+ config = found_variant.config
+ print(config.parameters)
+ return GetConfigReponse(
+ config_id=str(config.id),
+ config_name=config.config_name,
+ current_version=config.current_version,
+ parameters=config.parameters,
+ )
+ except HTTPException as e:
+ logger.error(f"get_config http exception: {e.detail}")
+ raise
+ except Exception as e:
+ logger.error(f"get_config exception: {e}")
+ raise HTTPException(status_code=500, detail=str(e)) from e
diff --git a/agenta-backend/agenta_backend/routers/container_router.py b/agenta-backend/agenta_backend/routers/container_router.py
index 80aa8a9fd7..d789aafc42 100644
--- a/agenta-backend/agenta_backend/routers/container_router.py
+++ b/agenta-backend/agenta_backend/routers/container_router.py
@@ -1,108 +1,173 @@
-import uuid
-import asyncio
-from pathlib import Path
-from typing import List, Union
-from fastapi import UploadFile, APIRouter
-from fastapi.responses import JSONResponse
-from agenta_backend.config import settings
-from aiodocker.exceptions import DockerError
-from concurrent.futures import ThreadPoolExecutor
-from agenta_backend.services.db_manager import get_templates
-from agenta_backend.models.api.api_models import Image, Template
-from agenta_backend.services.container_manager import (
- build_image_job,
- check_docker_arch,
- get_image_details_from_docker_hub,
- pull_image_from_docker_hub,
+import os
+from typing import List, Optional, Union
+from agenta_backend.models.api.api_models import (
+ URI,
+ Image,
+ RestartAppContainer,
+ Template,
)
+from agenta_backend.services import db_manager
+from fastapi import APIRouter, Request, UploadFile, HTTPException
+from fastapi.responses import JSONResponse
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+if os.environ["FEATURE_FLAG"] in ["cloud"]:
+ from agenta_backend.cloud.services import container_manager
+elif os.environ["FEATURE_FLAG"] in ["ee"]:
+ from agenta_backend.ee.services import container_manager
+else:
+ from agenta_backend.services import container_manager
+
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
router = APIRouter()
+# TODO: We need to improve this to use the introduced abstraction to also use start and stop service
@router.post("/build_image/")
-async def build_image(app_name: str, variant_name: str, tar_file: UploadFile) -> Image:
- """Takes a tar file and builds a docker image from it
+async def build_image(
+ app_id: str,
+ base_name: str,
+ tar_file: UploadFile,
+ request: Request,
+) -> Image:
+ """
+ Builds a Docker image from a tar file containing the application code.
- Arguments:
- app_name -- The `app_name` parameter is a string that represents the name of \
- the application for which the docker image is being built
- variant_name -- The `variant_name` parameter is a string that represents the \
- name or type of the variant for which the docker image is being built.
- tar_file -- The `tar_file` parameter is of type `UploadFile`. It represents the \
- uploaded tar file that will be used to build the Docker image
+ Args:
+ app_id (str): The ID of the application to build the image for.
+ base_name (str): The base name of the image to build.
+ tar_file (UploadFile): The tar file containing the application code.
+ stoken_session (SessionContainer): The session container for the user making the request.
Returns:
- an object of type `Image`.
+ Image: The Docker image that was built.
"""
+ # Get user and org id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
- loop = asyncio.get_event_loop()
+ # Check app access
+ app_db = await db_manager.fetch_app_and_check_access(
+ app_id=app_id, user_org_data=user_org_data
+ )
- # Create a ThreadPoolExecutor for running threads
- thread_pool = ThreadPoolExecutor(max_workers=4)
+ image_result = await container_manager.build_image(
+ app_db=app_db,
+ base_name=base_name,
+ tar_file=tar_file,
+ )
- # Create a unique temporary directory for each upload
- temp_dir = Path(f"/tmp/{uuid.uuid4()}")
- temp_dir.mkdir(parents=True, exist_ok=True)
+ return image_result
- # Save uploaded file to the temporary directory
- tar_path = temp_dir / tar_file.filename
- with tar_path.open("wb") as buffer:
- buffer.write(await tar_file.read())
- image_name = f"agentaai/{app_name.lower()}_{variant_name.lower()}:latest"
+@router.post("/restart_container/")
+async def restart_docker_container(
+ payload: RestartAppContainer,
+ request: Request,
+) -> dict:
+ """Restart docker container.
- # Use the thread pool to run the build_image_job function in a separate thread
- future = loop.run_in_executor(
- thread_pool,
- build_image_job,
- *(app_name, variant_name, tar_path, image_name, temp_dir),
+ Args:
+ payload (RestartAppContainer) -- the required data (app_name and variant_name)
+ """
+ logger.debug(f"Restarting container for variant {payload.variant_id}")
+ # Get user and org id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ app_variant_db = await db_manager.fetch_app_variant_and_check_access(
+ app_variant_id=payload.variant_id, user_org_data=user_org_data
)
+ try:
+ deployment = await db_manager.get_deployment_by_objectid(
+ app_variant_db.base.deployment
+ )
+ container_id = deployment.container_id
- # Return immediately while the image build is in progress
- image_result = await asyncio.wrap_future(future)
- return image_result
+ logger.debug(f"Restarting container with id: {container_id}")
+ container_manager.restart_container(container_id)
+ return {"message": "Please wait a moment. The container is now restarting."}
+ except Exception as ex:
+ return JSONResponse({"message": str(ex)}, status_code=500)
@router.get("/templates/")
-async def container_templates() -> Union[List[Template], str]:
- """Returns a list of container templates.
+async def container_templates(
+ request: Request,
+) -> Union[List[Template], str]:
+ """
+ Returns a list of templates available for creating new containers.
+
+ Parameters:
+ stoken_session (SessionContainer): The session container for the user.
Returns:
- a list of `Template` objects.
+
+ Union[List[Template], str]: A list of templates or an error message.
"""
- templates = get_templates()
+ try:
+ templates = await db_manager.get_templates()
+ except Exception as e:
+ return JSONResponse({"message": str(e)}, status_code=500)
return templates
-@router.get("/templates/{image_name}/images/")
-async def pull_image(image_name: str) -> dict:
- """Pulls an image from Docker Hub using the provided configuration
+@router.get("/container_url/")
+async def construct_app_container_url(
+ request: Request,
+ base_id: Optional[str] = None,
+ variant_id: Optional[str] = None,
+) -> URI:
+ """
+ Constructs the URL for an app container based on the provided base_id or variant_id.
- Arguments:
- image_name -- The name of the image to be pulled
+ Args:
+ base_id (Optional[str]): The ID of the base to use for the app container.
+ variant_id (Optional[str]): The ID of the variant to use for the app container.
+ stoken_session (SessionContainer): The session container for the user.
Returns:
- -- a JSON response with the image tag name and image ID
- -- a JSON response with the pull_image exception error
- """
- # Get docker hub config
- repo_owner = settings.docker_hub_repo_owner
- repo_name = settings.docker_hub_repo_name
+ URI: The URI for the app container.
- # Pull image from docker hub with provided config
- try:
- image_res = await pull_image_from_docker_hub(
- f"{repo_owner}/{repo_name}", image_name
+ Raises:
+ HTTPException: If the base or variant cannot be found or the user does not have access.
+ """
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ if base_id:
+ base_db = await db_manager.fetch_base_and_check_access(
+ base_id=base_id, user_org_data=user_org_data
+ )
+ # TODO: Add status check if base_db.status == "running"
+ if base_db.deployment:
+ deployment = await db_manager.get_deployment_by_objectid(base_db.deployment)
+ uri = deployment.uri
+ else:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Base {base_id} does not have a deployment",
+ )
+
+ return URI(uri=uri)
+ elif variant_id:
+ variant_db = await db_manager.fetch_app_variant_and_check_access(
+ app_variant_id=variant_id, user_org_data=user_org_data
)
- except DockerError as ext:
+ deployment = await db_manager.get_deployment_by_objectid(
+ variant_db.base.deployment
+ )
+ assert deployment and deployment.uri, "Deployment not found"
+ return URI(uri=deployment.uri)
+ else:
return JSONResponse(
- {"message": "Image with tag does not exist", "meta": str(ext)}, 404
+ {"detail": "Please provide either base_id or variant_id"},
+ status_code=400,
)
-
- # Get data from image response
- image_tag_name = image_res[0]["id"]
- image_id = await get_image_details_from_docker_hub(
- repo_owner, repo_name, image_tag_name
- )
- return JSONResponse({"image_tag": image_tag_name, "image_id": image_id}, 200)
diff --git a/agenta-backend/agenta_backend/routers/environment_router.py b/agenta-backend/agenta_backend/routers/environment_router.py
new file mode 100644
index 0000000000..3c68ec6349
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/environment_router.py
@@ -0,0 +1,63 @@
+import os
+from typing import List
+
+from fastapi.responses import JSONResponse
+from agenta_backend.services import db_manager
+from fastapi import APIRouter, Request, HTTPException
+from agenta_backend.utils.common import check_access_to_app, check_access_to_variant
+from agenta_backend.models.api.api_models import (
+ EnvironmentOutput,
+ DeployToEnvironmentPayload,
+)
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+router = APIRouter()
+
+
+@router.post("/deploy/")
+async def deploy_to_environment(
+ payload: DeployToEnvironmentPayload,
+ request: Request,
+):
+ """Deploys a given variant to an environment
+
+ Args:
+ environment_name: Name of the environment to deploy to.
+ variant_id: variant id to deploy.
+ stoken_session: . Defaults to Depends(verify_session()).
+
+ Raises:
+ HTTPException: If the deployment fails.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Check if has app access
+ access_app = await check_access_to_variant(
+ user_org_data, variant_id=payload.variant_id
+ )
+
+ if not access_app:
+ error_msg = f"You do not have access to this variant: {payload.variant_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ else:
+ await db_manager.deploy_to_environment(
+ environment_name=payload.environment_name,
+ variant_id=payload.variant_id,
+ **user_org_data,
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/agenta-backend/agenta_backend/routers/evaluation_router.py b/agenta-backend/agenta_backend/routers/evaluation_router.py
index 914393950d..408d3ba4bd 100644
--- a/agenta-backend/agenta_backend/routers/evaluation_router.py
+++ b/agenta-backend/agenta_backend/routers/evaluation_router.py
@@ -1,38 +1,64 @@
-from fastapi import HTTPException, APIRouter, Body
-from datetime import datetime
-from bson import ObjectId
-from typing import List, Optional
+import os
+import random
+from typing import List, Dict
+
+from fastapi.responses import JSONResponse
+from fastapi import HTTPException, APIRouter, Body, Request, status, Response
+
+from agenta_backend.services.helpers import format_inputs, format_outputs
from agenta_backend.models.api.evaluation_model import (
+ AICritiqueCreate,
+ CustomEvaluationNames,
Evaluation,
EvaluationScenario,
+ CustomEvaluationOutput,
+ CustomEvaluationDetail,
+ EvaluationScenarioScoreUpdate,
EvaluationScenarioUpdate,
+ ExecuteCustomEvaluationCode,
NewEvaluation,
DeleteEvaluation,
EvaluationType,
- EvaluationStatus,
-)
-from agenta_backend.services.results_service import (
- fetch_results_for_human_a_b_testing_evaluation,
- fetch_results_for_auto_exact_match_evaluation,
- fetch_results_for_auto_similarity_match_evaluation,
- fetch_results_for_auto_ai_critique,
+ CreateCustomEvaluation,
+ EvaluationUpdate,
+ EvaluationWebhook,
+ SimpleEvaluationOutput,
)
from agenta_backend.services.evaluation_service import (
UpdateEvaluationScenarioError,
+ evaluate_with_ai_critique,
+ fetch_custom_evaluation_names,
+ fetch_custom_evaluations,
+ fetch_custom_evaluation_detail,
+ get_evaluation_scenario_score,
update_evaluation_scenario,
- update_evaluation_status,
- create_new_evaluation,
-)
-from agenta_backend.services.db_mongo import (
- evaluations,
- evaluation_scenarios,
+ update_evaluation_scenario_score,
+ update_evaluation,
+ create_custom_code_evaluation,
+ update_custom_code_evaluation,
+ execute_custom_code_evaluation,
)
+from agenta_backend.services import evaluation_service
+from agenta_backend.utils.common import check_access_to_app
+from agenta_backend.services import db_manager
+from agenta_backend.models import converters
+from agenta_backend.services import results_service
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import ( # noqa pylint: disable-all
+ get_user_and_org_id,
+ )
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
router = APIRouter()
-@router.post("/", response_model=Evaluation)
-async def create_evaluation(newEvaluationData: NewEvaluation = Body(...)):
+@router.post("/", response_model=SimpleEvaluationOutput)
+async def create_evaluation(
+ payload: NewEvaluation,
+ request: Request,
+):
"""Creates a new comparison table document
Raises:
HTTPException: _description_
@@ -40,7 +66,27 @@ async def create_evaluation(newEvaluationData: NewEvaluation = Body(...)):
_description_
"""
try:
- return await create_new_evaluation(newEvaluationData)
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data,
+ app_id=payload.app_id,
+ check_owner=False,
+ )
+ if not access_app:
+ error_msg = f"You do not have access to this app: {payload.app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app = await db_manager.fetch_app_by_id(app_id=payload.app_id)
+
+ if app is None:
+ raise HTTPException(status_code=404, detail="App not found")
+
+ new_evaluation_db = await evaluation_service.create_new_evaluation(
+ payload, **user_org_data
+ )
+ return converters.evaluation_db_to_simple_evaluation_output(new_evaluation_db)
except KeyError:
raise HTTPException(
status_code=400,
@@ -48,18 +94,26 @@ async def create_evaluation(newEvaluationData: NewEvaluation = Body(...)):
)
-@router.put("/{evaluation_id}", response_model=Evaluation)
-async def update_evaluation_status_router(
- evaluation_id: str, update_data: EvaluationStatus = Body(...)
+@router.put("/{evaluation_id}/")
+async def update_evaluation_router(
+ request: Request,
+ evaluation_id: str,
+ update_data: EvaluationUpdate = Body(...),
):
- """Updates an evaluation status
+ """Updates an evaluation's status.
+
Raises:
- HTTPException: _description_
+ HTTPException: If the columns in the test set do not match with the inputs in the variant.
+
Returns:
- _description_
+ None: A 204 No Content status code, indicating that the update was successful.
"""
try:
- return await update_evaluation_status(evaluation_id, update_data.status)
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ await update_evaluation(evaluation_id, update_data, **user_org_data)
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
+
except KeyError:
raise HTTPException(
status_code=400,
@@ -68,118 +122,205 @@ async def update_evaluation_status_router(
@router.get(
- "/{evaluation_id}/evaluation_scenarios",
+ "/{evaluation_id}/evaluation_scenarios/",
response_model=List[EvaluationScenario],
)
-async def fetch_evaluation_scenarios(evaluation_id: str):
- """Creates an empty evaluation row
+async def fetch_evaluation_scenarios(
+ evaluation_id: str,
+ request: Request,
+):
+ """Fetches evaluation scenarios for a given evaluation ID.
Arguments:
- evaluation_scenario -- _description_
+ evaluation_id (str): The ID of the evaluation for which to fetch scenarios.
Raises:
- HTTPException: _description_
+ HTTPException: If the evaluation is not found or access is denied.
Returns:
- _description_
+ List[EvaluationScenario]: A list of evaluation scenarios.
"""
- cursor = evaluation_scenarios.find({"evaluation_id": evaluation_id})
- items = await cursor.to_list(length=100) # limit length to 100 for the example
- for item in items:
- item["id"] = str(item["_id"])
- return items
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ eval_scenarios = await evaluation_service.fetch_evaluation_scenarios_for_evaluation(
+ evaluation_id, **user_org_data
+ )
-@router.post("/{evaluation_id}/evaluation_scenario", response_model=EvaluationScenario)
-async def create_evaluation_scenario(evaluation_scenario: EvaluationScenario):
- """Creates an empty evaluation row
+ return eval_scenarios
- Arguments:
- evaluation_scenario -- _description_
+
+@router.post("/{evaluation_id}/evaluation_scenario/")
+async def create_evaluation_scenario(
+ evaluation_id: str,
+ evaluation_scenario: EvaluationScenario,
+ request: Request,
+):
+ """Create a new evaluation scenario for a given evaluation ID.
Raises:
- HTTPException: _description_
+ HTTPException: If evaluation not found or access denied.
Returns:
- _description_
+ None: 204 No Content status code upon success.
"""
- evaluation_scenario_dict = evaluation_scenario.dict()
- evaluation_scenario_dict.pop("id", None)
-
- evaluation_scenario_dict["created_at"] = evaluation_scenario_dict[
- "updated_at"
- ] = datetime.utcnow()
- result = await evaluation_scenarios.insert_one(evaluation_scenario_dict)
- if result.acknowledged:
- evaluation_scenario_dict["id"] = str(result.inserted_id)
- return evaluation_scenario_dict
- else:
- raise HTTPException(
- status_code=500, detail="Failed to create evaluation_scenario"
- )
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ await evaluation_service.create_evaluation_scenario(
+ evaluation_id, evaluation_scenario, **user_org_data
+ )
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.put(
- "/{evaluation_id}/evaluation_scenario/{evaluation_scenario_id}/{evaluation_type}"
+ "/{evaluation_id}/evaluation_scenario/{evaluation_scenario_id}/{evaluation_type}/"
)
async def update_evaluation_scenario_router(
evaluation_scenario_id: str,
- evaluation_scenario: EvaluationScenarioUpdate,
evaluation_type: EvaluationType,
+ evaluation_scenario: EvaluationScenarioUpdate,
+ request: Request,
):
- """Updates an evaluation row with a vote
-
- Arguments:
- evaluation_scenario_id -- _description_
- evaluation_scenario -- _description_
+ """Updates an evaluation scenario's vote or score based on its type.
Raises:
- HTTPException: _description_
+ HTTPException: If update fails or unauthorized.
Returns:
- _description_
+ None: 204 No Content status code upon successful update.
"""
+ user_org_data = await get_user_and_org_id(request.state.user_id)
try:
- return await update_evaluation_scenario(
- evaluation_scenario_id, evaluation_scenario, evaluation_type
+ await update_evaluation_scenario(
+ evaluation_scenario_id,
+ evaluation_scenario,
+ evaluation_type,
+ **user_org_data,
)
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
except UpdateEvaluationScenarioError as e:
raise HTTPException(status_code=500, detail=str(e)) from e
-@router.get("/", response_model=List[Evaluation])
-async def fetch_list_evaluations(app_name: Optional[str] = None):
- """lists of all comparison tables
+@router.post("/evaluation_scenario/ai_critique/", response_model=str)
+async def evaluate_ai_critique(
+ payload: AICritiqueCreate,
+ request: Request,
+) -> str:
+ """
+ Evaluate AI critique based on the given payload.
+
+ Args:
+ payload (AICritiqueCreate): The payload containing data for AI critique evaluation.
+ stoken_session (SessionContainer): The session container verified by `verify_session`.
Returns:
- _description_
+ str: The output of the AI critique evaluation.
+
+ Raises:
+ HTTPException: If any exception occurs during the evaluation.
+ """
+ try:
+ # Extract data from the payload
+ payload_dict = payload.dict()
+
+ # Run AI critique evaluation
+ output = evaluate_with_ai_critique(
+ llm_app_prompt_template=payload_dict["llm_app_prompt_template"],
+ llm_app_inputs=payload_dict["inputs"],
+ correct_answer=payload_dict["correct_answer"],
+ app_variant_output=payload_dict["outputs"][0]["variant_output"],
+ evaluation_prompt_template=payload_dict["evaluation_prompt_template"],
+ open_ai_key=payload_dict["open_ai_key"],
+ )
+ return output
+
+ except Exception as e:
+ raise HTTPException(400, f"Failed to evaluate AI critique: {str(e)}")
+
+
+@router.get("/evaluation_scenario/{evaluation_scenario_id}/score/")
+async def get_evaluation_scenario_score_router(
+ evaluation_scenario_id: str,
+ request: Request,
+) -> Dict[str, str]:
+ """
+ Fetch the score of a specific evaluation scenario.
+
+ Args:
+ evaluation_scenario_id: The ID of the evaluation scenario to fetch.
+ stoken_session: Session data, verified by `verify_session`.
+
+ Returns:
+ Dictionary containing the scenario ID and its score.
"""
- cursor = evaluations.find({"app_name": app_name}).sort("created_at", -1)
- items = await cursor.to_list(length=100) # limit length to 100 for the example
- for item in items:
- item["id"] = str(item["_id"])
- return items
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ return await get_evaluation_scenario_score(evaluation_scenario_id, **user_org_data)
-@router.get("/{evaluation_id}", response_model=Evaluation)
-async def fetch_evaluation(evaluation_id: str):
- """Fetch one comparison table
+@router.put("/evaluation_scenario/{evaluation_scenario_id}/score/")
+async def update_evaluation_scenario_score_router(
+ evaluation_scenario_id: str,
+ payload: EvaluationScenarioScoreUpdate,
+ request: Request,
+):
+ """Updates the score of an evaluation scenario.
+
+ Raises:
+ HTTPException: Server error if the evaluation update fails.
Returns:
- _description_
+ None: 204 No Content status code upon successful update.
"""
- evaluation = await evaluations.find_one({"_id": ObjectId(evaluation_id)})
- if evaluation:
- evaluation["id"] = str(evaluation["_id"])
- return evaluation
- else:
- raise HTTPException(
- status_code=404, detail=f"dataset with id {evaluation_id} not found"
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ try:
+ await update_evaluation_scenario_score(
+ evaluation_scenario_id, payload.score, **user_org_data
)
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e)) from e
+
+
+@router.get("/", response_model=List[Evaluation])
+async def fetch_list_evaluations(
+ app_id: str,
+ request: Request,
+):
+ """Fetches a list of evaluations, optionally filtered by an app ID.
+
+ Args:
+ app_id (Optional[str]): An optional app ID to filter the evaluations.
+
+ Returns:
+ List[Evaluation]: A list of evaluations.
+ """
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ return await evaluation_service.fetch_list_evaluations(
+ app_id=app_id, **user_org_data
+ )
+
+
+@router.get("/{evaluation_id}/", response_model=Evaluation)
+async def fetch_evaluation(
+ evaluation_id: str,
+ request: Request,
+):
+ """Fetches a single evaluation based on its ID.
+
+ Args:
+ evaluation_id (str): The ID of the evaluation to fetch.
+
+ Returns:
+ Evaluation: The fetched evaluation.
+ """
+ user_org_data = await get_user_and_org_id(request.state.user_id)
+ return await evaluation_service.fetch_evaluation(evaluation_id, **user_org_data)
@router.delete("/", response_model=List[str])
-async def delete_evaluations(delete_evaluations: DeleteEvaluation):
+async def delete_evaluations(
+ delete_evaluations: DeleteEvaluation,
+ request: Request,
+):
"""
Delete specific comparison tables based on their unique IDs.
@@ -189,26 +330,20 @@ async def delete_evaluations(delete_evaluations: DeleteEvaluation):
Returns:
A list of the deleted comparison tables' IDs.
"""
- deleted_ids = []
-
- for evaluations_id in delete_evaluations.evaluations_ids:
- evaluation = await evaluations.find_one({"_id": ObjectId(evaluations_id)})
-
- if evaluation is not None:
- result = await evaluations.delete_one({"_id": ObjectId(evaluations_id)})
- if result:
- deleted_ids.append(evaluations_id)
- else:
- raise HTTPException(
- status_code=404,
- detail=f"Comparison table {evaluations_id} not found",
- )
- return deleted_ids
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ await evaluation_service.delete_evaluations(
+ delete_evaluations.evaluations_ids, **user_org_data
+ )
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
-@router.get("/{evaluation_id}/results")
-async def fetch_results(evaluation_id: str):
+@router.get("/{evaluation_id}/results/")
+async def fetch_results(
+ evaluation_id: str,
+ request: Request,
+):
"""Fetch all the results for one the comparison table
Arguments:
@@ -217,27 +352,229 @@ async def fetch_results(evaluation_id: str):
Returns:
_description_
"""
- evaluation = await evaluations.find_one({"_id": ObjectId(evaluation_id)})
- if evaluation["evaluation_type"] == EvaluationType.human_a_b_testing:
- results = await fetch_results_for_human_a_b_testing_evaluation(
- evaluation_id, evaluation.get("variants", [])
- )
- # TODO: replace votes_data by results_data
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ evaluation = await evaluation_service._fetch_evaluation_and_check_access(
+ evaluation_id, **user_org_data
+ )
+ if evaluation.evaluation_type == EvaluationType.human_a_b_testing:
+ results = await results_service.fetch_results_for_evaluation(evaluation)
return {"votes_data": results}
- elif evaluation["evaluation_type"] == EvaluationType.auto_exact_match:
- results = await fetch_results_for_auto_exact_match_evaluation(
- evaluation_id, evaluation.get("variant", [])
- )
+ elif evaluation.evaluation_type == EvaluationType.auto_exact_match:
+ results = await results_service.fetch_results_for_evaluation(evaluation)
return {"scores_data": results}
- elif evaluation["evaluation_type"] == EvaluationType.auto_similarity_match:
- results = await fetch_results_for_auto_similarity_match_evaluation(
- evaluation_id, evaluation.get("variant", [])
- )
+ elif evaluation.evaluation_type == EvaluationType.auto_similarity_match:
+ results = await results_service.fetch_results_for_evaluation(evaluation)
return {"scores_data": results}
- elif evaluation["evaluation_type"] == EvaluationType.auto_ai_critique:
- results = await fetch_results_for_auto_ai_critique(evaluation_id)
+ elif evaluation.evaluation_type == EvaluationType.auto_regex_test:
+ results = await results_service.fetch_results_for_evaluation(evaluation)
+ return {"scores_data": results}
+
+ elif evaluation.evaluation_type == EvaluationType.auto_webhook_test:
+ results = await results_service.fetch_results_for_auto_ai_critique(
+ evaluation_id
+ )
+ return {"results_data": results}
+
+ elif evaluation.evaluation_type == EvaluationType.single_model_test:
+ results = await results_service.fetch_results_for_auto_ai_critique(
+ evaluation_id
+ )
return {"results_data": results}
+
+ elif evaluation.evaluation_type == EvaluationType.auto_ai_critique:
+ results = await results_service.fetch_results_for_auto_ai_critique(
+ evaluation_id
+ )
+ return {"results_data": results}
+
+ elif evaluation.evaluation_type == EvaluationType.custom_code_run:
+ results = await results_service.fetch_average_score_for_custom_code_run(
+ evaluation_id
+ )
+ return {"avg_score": results}
+
+
+@router.post("/custom_evaluation/")
+async def create_custom_evaluation(
+ custom_evaluation_payload: CreateCustomEvaluation,
+ request: Request,
+):
+ """Create evaluation with custom python code.
+
+ Args:
+ \n custom_evaluation_payload (CreateCustomEvaluation): the required payload
+ """
+
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # create custom evaluation in database
+ evaluation_id = await create_custom_code_evaluation(
+ custom_evaluation_payload, **user_org_data
+ )
+
+ return JSONResponse(
+ {
+ "status": "success",
+ "message": "Evaluation created successfully.",
+ "evaluation_id": evaluation_id,
+ },
+ status_code=200,
+ )
+
+
+@router.put("/custom_evaluation/{id}")
+async def update_custom_evaluation(
+ id: str,
+ updated_data: CreateCustomEvaluation,
+ request: Request,
+):
+ """Update a custom code evaluation.
+ Args:
+ id (str): the ID of the custom evaluation to update
+ updated_data (CreateCustomEvaluation): the payload with updated data
+ stoken_session (SessionContainer): session container for authentication
+ """
+
+ # Get user and organization id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Update the evaluation with the provided data
+ updated_evaluation_id = await update_custom_code_evaluation(
+ id, updated_data, **kwargs
+ )
+
+ return JSONResponse(
+ {
+ "status": "success",
+ "message": "Evaluation edited successfully.",
+ "evaluation_id": updated_evaluation_id,
+ },
+ status_code=200,
+ )
+
+
+@router.get(
+ "/custom_evaluation/list/{app_id}/",
+ response_model=List[CustomEvaluationOutput],
+)
+async def list_custom_evaluations(
+ app_id: str,
+ request: Request,
+):
+ """List the custom code evaluations for a given app.
+
+ Args:
+ app_id (str): the id of the app
+
+ Returns:
+ List[CustomEvaluationOutput]: a list of custom evaluation
+ """
+
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Fetch custom evaluations from database
+ evaluations = await fetch_custom_evaluations(app_id, **user_org_data)
+ return evaluations
+
+
+@router.get(
+ "/custom_evaluation/{id}/",
+ response_model=CustomEvaluationDetail,
+)
+async def get_custom_evaluation(
+ id: str,
+ request: Request,
+):
+ """Get the custom code evaluation detail.
+
+ Args:
+ id (str): the id of the custom evaluation
+
+ Returns:
+ CustomEvaluationDetail: Detail of the custom evaluation
+ """
+
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Fetch custom evaluations from database
+ evaluation = await fetch_custom_evaluation_detail(id, **user_org_data)
+ return evaluation
+
+
+@router.get(
+ "/custom_evaluation/{app_name}/names/",
+ response_model=List[CustomEvaluationNames],
+)
+async def get_custom_evaluation_names(app_name: str, request: Request):
+ """Get the names of custom evaluation for a given app.
+
+ Args:
+ app_name (str): the name of the app the evaluation belongs to
+
+ Returns:
+ List[CustomEvaluationNames]: the list of name of custom evaluations
+ """
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ custom_eval_names = await fetch_custom_evaluation_names(app_name, **user_org_data)
+ return custom_eval_names
+
+
+@router.post(
+ "/custom_evaluation/execute/{evaluation_id}/",
+)
+async def execute_custom_evaluation(
+ evaluation_id: str,
+ payload: ExecuteCustomEvaluationCode,
+ request: Request,
+):
+ """Execute a custom evaluation code.
+
+ Args:
+ evaluation_id (str): the custom evaluation id
+ payload (ExecuteCustomEvaluationCode): the required payload
+
+ Returns:
+ float: the result of the evaluation custom code
+ """
+
+ # Get user and organization id
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Execute custom code evaluation
+ formatted_inputs = format_inputs(payload.inputs)
+ formatted_outputs = format_outputs(payload.outputs)
+ output = list(formatted_outputs.values())[
+ 0
+ ] # for now we expect one output as a string
+ result = await execute_custom_code_evaluation(
+ evaluation_id,
+ payload.app_id,
+ output,
+ payload.correct_answer,
+ payload.variant_id,
+ formatted_inputs,
+ **user_org_data,
+ )
+ return result
+
+
+@router.post("/webhook_example_fake/", response_model=EvaluationWebhook)
+async def webhook_example_fake():
+ """Returns a fake score response for example webhook evaluation
+
+ Returns:
+ _description_
+ """
+
+ # return a random score b/w 0 and 1
+ return {"score": random.random()}
diff --git a/agenta-backend/agenta_backend/routers/health_router.py b/agenta-backend/agenta_backend/routers/health_router.py
new file mode 100644
index 0000000000..e59dc08e0f
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/health_router.py
@@ -0,0 +1,8 @@
+from fastapi import APIRouter, status
+
+router = APIRouter()
+
+
+@router.get("/", status_code=status.HTTP_200_OK)
+def health_check():
+ return {"status": "ok"}
diff --git a/agenta-backend/agenta_backend/routers/observability_router.py b/agenta-backend/agenta_backend/routers/observability_router.py
new file mode 100644
index 0000000000..1896005492
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/observability_router.py
@@ -0,0 +1,150 @@
+import os
+from typing import List
+
+from fastapi import APIRouter, Request
+
+from agenta_backend.services.event_db_manager import (
+ get_variant_traces,
+ create_app_trace,
+ create_trace_span,
+ get_single_trace,
+ trace_status_update,
+ get_trace_spans,
+ add_feedback_to_trace,
+ get_trace_feedbacks,
+ get_feedback_detail,
+ update_trace_feedback,
+)
+from agenta_backend.models.api.observability_models import (
+ Span,
+ CreateSpan,
+ CreateFeedback,
+ Feedback,
+ UpdateFeedback,
+ Trace,
+ CreateTrace,
+ UpdateTrace,
+)
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+router = APIRouter()
+
+
+@router.post("/traces/", response_model=str)
+async def create_trace(
+ payload: CreateTrace,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ trace = await create_app_trace(payload, **kwargs)
+ return trace
+
+
+@router.get("/traces/{app_id}/{variant_id}/", response_model=List[Trace])
+async def get_traces(
+ app_id: str,
+ variant_id: str,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ traces = await get_variant_traces(app_id, variant_id, **kwargs)
+ return traces
+
+
+@router.get("/traces/{trace_id}/", response_model=Trace)
+async def get_trace(
+ trace_id: str,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ trace = await get_single_trace(trace_id, **kwargs)
+ return trace
+
+
+@router.post("/spans/", response_model=str)
+async def create_span(
+ payload: CreateSpan,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ spans_id = await create_trace_span(payload, **kwargs)
+ return spans_id
+
+
+@router.get("/spans/{trace_id}/", response_model=List[Span])
+async def get_spans_of_trace(
+ trace_id: str,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ spans = await get_trace_spans(trace_id, **kwargs)
+ return spans
+
+
+@router.put("/traces/{trace_id}/", response_model=bool)
+async def update_trace_status(
+ trace_id: str,
+ payload: UpdateTrace,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ trace = await trace_status_update(trace_id, payload, **kwargs)
+ return trace
+
+
+@router.post("/feedbacks/{trace_id}/", response_model=str)
+async def create_feedback(
+ trace_id: str,
+ payload: CreateFeedback,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ feedback = await add_feedback_to_trace(trace_id, payload, **kwargs)
+ return feedback
+
+
+@router.get("/feedbacks/{trace_id}/", response_model=List[Feedback])
+async def get_feedbacks(trace_id: str, request: Request):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ feedbacks = await get_trace_feedbacks(trace_id, **kwargs)
+ return feedbacks
+
+
+@router.get("/feedbacks/{trace_id}/{feedback_id}/", response_model=Feedback)
+async def get_feedback(
+ trace_id: str,
+ feedback_id: str,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ feedback = await get_feedback_detail(trace_id, feedback_id, **kwargs)
+ return feedback
+
+
+@router.put("/feedbacks/{trace_id}/{feedback_id}/", response_model=Feedback)
+async def update_feedback(
+ trace_id: str,
+ feedback_id: str,
+ payload: UpdateFeedback,
+ request: Request,
+):
+ # Get user and org id
+ kwargs: dict = await get_user_and_org_id(request.state.user_id)
+ feedback = await update_trace_feedback(trace_id, feedback_id, payload, **kwargs)
+ return feedback
diff --git a/agenta-backend/agenta_backend/routers/organization_router.py b/agenta-backend/agenta_backend/routers/organization_router.py
new file mode 100644
index 0000000000..c78f9606b6
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/organization_router.py
@@ -0,0 +1,80 @@
+"""Routes for image-related operations (push, remove).
+Does not deal with the instanciation of the images
+"""
+
+import os
+import logging
+from fastapi import APIRouter, HTTPException, Request
+from agenta_backend.services.selectors import get_user_own_org
+from agenta_backend.models.api.organization_models import (
+ OrganizationOutput,
+ Organization,
+)
+from agenta_backend.services import db_manager
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+@router.get("/", response_model=list[Organization])
+async def list_organizations(
+ request: Request,
+):
+ """
+ Returns a list of organizations associated with the user's session.
+
+ Args:
+ stoken_session (SessionContainer): The user's session token.
+
+ Returns:
+ list[Organization]: A list of organizations associated with the user's session.
+
+ Raises:
+ HTTPException: If there is an error retrieving the organizations from the database.
+ """
+
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ organizations_db = await db_manager.get_organizations_by_list_ids(
+ user_org_data["organization_ids"]
+ )
+ response = [
+ Organization(
+ id=str(org.id),
+ name=str(org.name),
+ description=str(org.description),
+ owner=str(org.owner),
+ ).dict(exclude_unset=True)
+ for org in organizations_db
+ ]
+
+ return response
+
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=str(e),
+ )
+
+
+@router.get("/own/")
+async def get_user_organization(
+ request: Request,
+):
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ org_db = await get_user_own_org(user_org_data["uid"])
+ if org_db is None:
+ raise HTTPException(404, detail="User does not have an organization")
+ return OrganizationOutput(id=str(org_db.id), name=org_db.name)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/agenta-backend/agenta_backend/routers/testset_router.py b/agenta-backend/agenta_backend/routers/testset_router.py
index 1c72203c4e..21e4ff37c5 100644
--- a/agenta-backend/agenta_backend/routers/testset_router.py
+++ b/agenta-backend/agenta_backend/routers/testset_router.py
@@ -1,27 +1,47 @@
-from fastapi import HTTPException, APIRouter, UploadFile, File, Form, Body
-from agenta_backend.services.db_mongo import testsets
+import io
+import os
+import csv
+import json
+import requests
+from bson import ObjectId
+from datetime import datetime
+from typing import Optional, List
+
+from fastapi import HTTPException, APIRouter, UploadFile, File, Form, Request
+from fastapi.responses import JSONResponse
+
from agenta_backend.models.api.testset_model import (
- UploadResponse,
+ TestSetSimpleResponse,
DeleteTestsets,
NewTestset,
+ TestSetOutputResponse,
)
-from datetime import datetime
-from typing import Optional, List
-from bson import ObjectId
-import csv
-import json
+from agenta_backend.utils.common import engine, check_access_to_app
+from agenta_backend.models.db_models import TestSetDB
+from agenta_backend.services.db_manager import get_user
+from agenta_backend.services import db_manager
+from agenta_backend.models.converters import testset_db_to_pydantic
upload_folder = "./path/to/upload/folder"
router = APIRouter()
-@router.post("/upload", response_model=UploadResponse)
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+@router.post("/upload/", response_model=TestSetSimpleResponse)
async def upload_file(
+ request: Request,
upload_type: str = Form(None),
file: UploadFile = File(...),
testset_name: Optional[str] = File(None),
- app_name: str = Form(None),
+ app_id: str = Form(None),
):
"""
Uploads a CSV or JSON file and saves its data to MongoDB.
@@ -35,55 +55,144 @@ async def upload_file(
dict: The result of the upload process.
"""
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id, check_owner=False
+ )
+ if not access_app:
+ error_msg = f"You do not have access to this app: {app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+ # Create a document
+ document = {
+ "created_at": datetime.now().isoformat(),
+ "name": testset_name if testset_name else file.filename,
+ "app": app,
+ "organization": app.organization,
+ "csvdata": [],
+ }
+
+ if upload_type == "JSON":
+ # Read and parse the JSON file
+ json_data = await file.read()
+ json_text = json_data.decode("utf-8")
+ json_object = json.loads(json_text)
+
+ # Populate the document with column names and values
+ for row in json_object:
+ document["csvdata"].append(row)
+
+ else:
+ # Read and parse the CSV file
+ csv_data = await file.read()
+ csv_text = csv_data.decode("utf-8")
+
+ # Use StringIO to create a file-like object from the string
+ csv_file_like_object = io.StringIO(csv_text)
+ csv_reader = csv.DictReader(csv_file_like_object)
+
+ # Populate the document with rows from the CSV reader
+ for row in csv_reader:
+ document["csvdata"].append(row)
+
+ user = await get_user(user_uid=user_org_data["uid"])
+ testset_instance = TestSetDB(**document, user=user)
+ result = await engine.save(testset_instance)
+
+ if isinstance(result.id, ObjectId):
+ return TestSetSimpleResponse(
+ id=str(result.id),
+ name=document["name"],
+ created_at=document["created_at"],
+ )
+
+
+@router.post("/endpoint/", response_model=TestSetSimpleResponse)
+async def import_testset(
+ request: Request,
+ endpoint: str = Form(None),
+ testset_name: str = Form(None),
+ app_id: str = Form(None),
+):
+ """
+ Import JSON testset data from an endpoint and save it to MongoDB.
+
+ Args:
+ endpoint (str): An endpoint URL to import data from.
+ testset_name (str): the name of the testset if provided.
+
+ Returns:
+ dict: The result of the import process.
+ """
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id, check_owner=False
+ )
+ if not access_app:
+ error_msg = f"You do not have access to this app: {app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+
try:
+ response = requests.get(endpoint, timeout=10)
+
+ if response.status_code != 200:
+ raise HTTPException(
+ status_code=400, detail="Failed to fetch testset from endpoint"
+ )
+
# Create a document
document = {
"created_at": datetime.now().isoformat(),
- "name": testset_name if testset_name else file.filename,
- "app_name": app_name,
+ "name": testset_name,
+ "app": app,
+ "organization": app.organization,
"csvdata": [],
}
- if upload_type == "JSON":
- # Read and parse the JSON file
- json_data = await file.read()
- json_text = json_data.decode("utf-8")
- json_object = json.loads(json_text)
+ # Populate the document with column names and values
+ json_response = response.json()
+ for row in json_response:
+ document["csvdata"].append(row)
- # Populate the document with column names and values
- for row in json_object:
- document["csvdata"].append(row)
+ user = await get_user(user_uid=user_org_data["uid"])
+ testset_instance = TestSetDB(**document, user=user)
+ result = await engine.save(testset_instance)
- else:
- # Read and parse the CSV file
- csv_data = await file.read()
- csv_text = csv_data.decode("utf-8")
- csv_reader = csv.reader(csv_text.splitlines())
- columns = next(csv_reader) # Get the column names
-
- # Populate the document with column names and values
- for row in csv_reader:
- row_data = {}
- for i, value in enumerate(row):
- row_data[columns[i]] = value
- document["csvdata"].append(row_data)
-
- result = await testsets.insert_one(document)
-
- if result.acknowledged:
- return UploadResponse(
- id=str(result.inserted_id),
+ if isinstance(result.id, ObjectId):
+ return TestSetSimpleResponse(
+ id=str(result.id),
name=document["name"],
created_at=document["created_at"],
)
- except Exception as e:
- print(e)
- raise HTTPException(status_code=500, detail="Failed to process file") from e
+ except HTTPException as error:
+ print(error)
+ raise error
+ except json.JSONDecodeError as error:
+ print(error)
+ raise HTTPException(
+ status_code=400, detail="Endpoint does not return valid JSON testset data"
+ ) from error
+ except Exception as error:
+ print(error)
+ raise HTTPException(
+ status_code=500, detail="Failed to import testset from endpoint"
+ ) from error
-@router.post("/{app_name}")
-async def create_testset(app_name: str, csvdata: NewTestset):
+@router.post("/{app_id}/")
+async def create_testset(
+ app_id: str,
+ csvdata: NewTestset,
+ request: Request,
+):
"""
Create a testset with given name and app_name, save the testset to MongoDB.
@@ -95,24 +204,49 @@ async def create_testset(app_name: str, csvdata: NewTestset):
Returns:
str: The id of the test set created.
"""
+
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ user = await get_user(user_uid=user_org_data["uid"])
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id, check_owner=False
+ )
+ if not access_app:
+ error_msg = f"You do not have access to this app: {app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
testset = {
- "name": csvdata.name,
- "app_name": app_name,
"created_at": datetime.now().isoformat(),
+ "name": csvdata.name,
+ "app": app,
+ "organization": app.organization,
"csvdata": csvdata.csvdata,
+ "user": user,
}
+
try:
- result = await testsets.insert_one(testset)
- if result.acknowledged:
- testset["_id"] = str(result.inserted_id)
- return testset
+ testset_instance = TestSetDB(**testset)
+ await engine.save(testset_instance)
+
+ if testset_instance is not None:
+ return TestSetSimpleResponse(
+ id=str(testset_instance.id),
+ name=testset_instance.name,
+ created_at=str(testset_instance.created_at),
+ )
except Exception as e:
print(str(e))
raise HTTPException(status_code=500, detail=str(e))
-@router.put("/{testset_id}")
-async def update_testset(testset_id: str, csvdata: NewTestset):
+@router.put("/{testset_id}/")
+async def update_testset(
+ testset_id: str,
+ csvdata: NewTestset,
+ request: Request,
+):
"""
Update a testset with given id, update the testset in MongoDB.
@@ -123,16 +257,30 @@ async def update_testset(testset_id: str, csvdata: NewTestset):
Returns:
str: The id of the test set updated.
"""
- testset = {
+ testset_update = {
"name": csvdata.name,
"csvdata": csvdata.csvdata,
"updated_at": datetime.now().isoformat(),
}
- try:
- result = await testsets.update_one(
- {"_id": ObjectId(testset_id)}, {"$set": testset}
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ test_set = await db_manager.fetch_testset_by_id(testset_id=testset_id)
+ if test_set is None:
+ raise HTTPException(status_code=404, detail="testset not found")
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=str(test_set.app.id), check_owner=False
+ )
+ if not access_app:
+ error_msg = f"You do not have access to this app: {test_set.app.id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
)
- if result.acknowledged:
+ try:
+ test_set.update(testset_update)
+ await engine.save(test_set)
+
+ if isinstance(test_set.id, ObjectId):
return {
"status": "success",
"message": "testset updated successfully",
@@ -145,8 +293,11 @@ async def update_testset(testset_id: str, csvdata: NewTestset):
raise HTTPException(status_code=500, detail=str(e))
-@router.get("/")
-async def get_testsets(app_name: Optional[str] = None):
+@router.get("/", tags=["testsets"])
+async def get_testsets(
+ app_id: str,
+ request: Request,
+) -> List[TestSetOutputResponse]:
"""
Get all testsets.
@@ -156,17 +307,37 @@ async def get_testsets(app_name: Optional[str] = None):
Raises:
- `HTTPException` with status code 404 if no testsets are found.
"""
- cursor = testsets.find(
- {"app_name": app_name}, {"_id": 1, "name": 1, "created_at": 1}
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=app_id, check_owner=False
)
- documents = await cursor.to_list(length=100)
- for document in documents:
- document["_id"] = str(document["_id"])
- return documents
+ if not access_app:
+ error_msg = f"You do not have access to this app: {app_id}"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+ if app is None:
+ raise HTTPException(status_code=404, detail="App not found")
+
+ testsets: List[TestSetDB] = await db_manager.fetch_testsets_by_app_id(app_id=app_id)
+ return [
+ TestSetOutputResponse(
+ id=str(testset.id),
+ name=testset.name,
+ created_at=testset.created_at,
+ )
+ for testset in testsets
+ ]
-@router.get("/{testset_id}", tags=["testsets"])
-async def get_testset(testset_id: str):
+
+@router.get("/{testset_id}/", tags=["testsets"])
+async def get_testset(
+ testset_id: str,
+ request: Request,
+):
"""
Fetch a specific testset in a MongoDB collection using its _id.
@@ -176,19 +347,27 @@ async def get_testset(testset_id: str):
Returns:
The requested testset if found, else an HTTPException.
"""
- testset = await testsets.find_one({"_id": ObjectId(testset_id)})
-
- if testset:
- testset["_id"] = str(testset["_id"])
- return testset
- else:
- raise HTTPException(
- status_code=404, detail=f"testset with id {testset_id} not found"
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ test_set = await db_manager.fetch_testset_by_id(testset_id=testset_id)
+ if test_set is None:
+ raise HTTPException(status_code=404, detail="testset not found")
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data, app_id=str(test_set.app.id), check_owner=False
+ )
+ if not access_app:
+ error_msg = "You do not have access to this test set"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
)
+ return testset_db_to_pydantic(test_set)
@router.delete("/", response_model=List[str])
-async def delete_testsets(delete_testsets: DeleteTestsets):
+async def delete_testsets(
+ delete_testsets: DeleteTestsets,
+ request: Request,
+):
"""
Delete specific testsets based on their unique IDs.
@@ -198,18 +377,26 @@ async def delete_testsets(delete_testsets: DeleteTestsets):
Returns:
A list of the deleted testsets' IDs.
"""
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
deleted_ids = []
for testset_id in delete_testsets.testset_ids:
- testset = await testsets.find_one({"_id": ObjectId(testset_id)})
-
- if testset is not None:
- result = await testsets.delete_one({"_id": ObjectId(testset_id)})
- if result:
- deleted_ids.append(testset_id)
- else:
- raise HTTPException(
- status_code=404, detail=f"testset {testset_id} not found"
+ test_set = await db_manager.fetch_testset_by_id(testset_id=testset_id)
+ if test_set is None:
+ raise HTTPException(status_code=404, detail="testset not found")
+ access_app = await check_access_to_app(
+ user_org_data=user_org_data,
+ app_id=str(test_set.app.id),
+ check_owner=False,
+ )
+ if not access_app:
+ error_msg = "You do not have access to this test set"
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
)
+ await engine.delete(test_set)
+ deleted_ids.append(testset_id)
return deleted_ids
diff --git a/agenta-backend/agenta_backend/routers/user_profile.py b/agenta-backend/agenta_backend/routers/user_profile.py
new file mode 100644
index 0000000000..9af12d6226
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/user_profile.py
@@ -0,0 +1,32 @@
+import os
+from agenta_backend.models.db_models import UserDB
+from fastapi import APIRouter, HTTPException, Request
+from agenta_backend.models.api.user_models import User
+from agenta_backend.services import db_manager
+
+router = APIRouter()
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+
+@router.get("/")
+async def user_profile(
+ request: Request,
+):
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ user = await db_manager.get_user(user_uid=user_org_data["uid"])
+ return User(
+ id=str(user.id),
+ uid=str(user.uid),
+ username=str(user.username),
+ email=str(user.email),
+ ).dict(exclude_unset=True)
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/agenta-backend/agenta_backend/routers/variants_router.py b/agenta-backend/agenta_backend/routers/variants_router.py
new file mode 100644
index 0000000000..503a8ce541
--- /dev/null
+++ b/agenta-backend/agenta_backend/routers/variants_router.py
@@ -0,0 +1,277 @@
+import os
+import logging
+from docker.errors import DockerException
+from fastapi.responses import JSONResponse
+from typing import Any, Optional, Union
+from fastapi import APIRouter, HTTPException, Request, Body
+from agenta_backend.services import (
+ app_manager,
+ db_manager,
+)
+from agenta_backend.utils.common import (
+ check_access_to_variant,
+)
+from agenta_backend.models import converters
+
+from agenta_backend.models.api.api_models import (
+ Image,
+ URI,
+ DockerEnvVars,
+ AddVariantFromBasePayload,
+ AppVariantOutput,
+ UpdateVariantParameterPayload,
+ VariantAction,
+ VariantActionEnum,
+)
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services.selectors import (
+ get_user_and_org_id,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services.selectors import get_user_and_org_id
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+@router.post("/from-base/")
+async def add_variant_from_base_and_config(
+ payload: AddVariantFromBasePayload,
+ request: Request,
+) -> Union[AppVariantOutput, Any]:
+ """Add a new variant based on an existing one.
+ Same as POST /config
+
+ Args:
+ payload (AddVariantFromBasePayload): Payload containing base variant ID, new variant name, and parameters.
+ stoken_session (SessionContainer, optional): Session container. Defaults to result of verify_session().
+
+ Raises:
+ HTTPException: Raised if the variant could not be added or accessed.
+
+ Returns:
+ Union[AppVariantOutput, Any]: New variant details or exception.
+ """
+ try:
+ logger.debug("Initiating process to add a variant based on a previous one.")
+ logger.debug(f"Received payload: {payload}")
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ base_db = await db_manager.fetch_base_and_check_access(
+ payload.base_id, user_org_data
+ )
+
+ # Find the previous variant in the database
+
+ db_app_variant = await db_manager.add_variant_from_base_and_config(
+ base_db=base_db,
+ new_config_name=payload.new_config_name,
+ parameters=payload.parameters,
+ **user_org_data,
+ )
+ logger.debug(f"Successfully added new variant: {db_app_variant}")
+ return await converters.app_variant_db_to_output(db_app_variant)
+
+ except Exception as e:
+ logger.error(f"An exception occurred while adding the new variant: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.delete("/{variant_id}/")
+async def remove_variant(
+ variant_id: str,
+ request: Request,
+):
+ """Remove a variant from the server.
+ In the case it's the last variant using the image, stop the container and remove the image.
+
+ Arguments:
+ app_variant -- AppVariant to remove
+
+ Raises:
+ HTTPException: If there is a problem removing the app variant
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Check app access
+
+ access_app = await check_access_to_variant(
+ user_org_data, variant_id=variant_id, check_owner=True
+ )
+
+ if not access_app:
+ error_msg = (
+ f"You do not have permission to delete app variant: {variant_id}"
+ )
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ else:
+ await app_manager.terminate_and_remove_app_variant(
+ app_variant_id=variant_id, **user_org_data
+ )
+ except DockerException as e:
+ detail = f"Docker error while trying to remove the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except Exception as e:
+ detail = f"Unexpected error while trying to remove the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+
+
+@router.put("/{variant_id}/parameters/")
+async def update_variant_parameters(
+ request: Request,
+ variant_id: str,
+ payload: UpdateVariantParameterPayload = Body(...),
+):
+ """
+ Updates the parameters for an app variant.
+
+ Args:
+ variant_id (str): The ID of the app variant to update.
+ payload (UpdateVariantParameterPayload): The payload containing the updated parameters.
+ stoken_session (SessionContainer, optional): The session container. Defaults to Depends(verify_session()).
+
+ Raises:
+ HTTPException: If there is an error while trying to update the app variant.
+
+ Returns:
+ JSONResponse: A JSON response containing the updated app variant parameters.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_variant = await check_access_to_variant(
+ user_org_data=user_org_data, variant_id=variant_id
+ )
+
+ if not access_variant:
+ error_msg = (
+ f"You do not have permission to update app variant: {variant_id}"
+ )
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ else:
+ await app_manager.update_variant_parameters(
+ app_variant_id=variant_id,
+ parameters=payload.parameters,
+ **user_org_data,
+ )
+ except ValueError as e:
+ detail = f"Error while trying to update the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except Exception as e:
+ detail = f"Unexpected error while trying to update the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+
+
+@router.put("/{variant_id}/image/")
+async def update_variant_image(
+ variant_id: str,
+ image: Image,
+ request: Request,
+):
+ """
+ Updates the image used in an app variant.
+
+ Args:
+ variant_id (str): The ID of the app variant to update.
+ image (Image): The image information to update.
+
+ Raises:
+ HTTPException: If an error occurs while trying to update the app variant.
+
+ Returns:
+ JSONResponse: A JSON response indicating whether the update was successful or not.
+ """
+ try:
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+ access_variant = await check_access_to_variant(
+ user_org_data=user_org_data, variant_id=variant_id
+ )
+ if not access_variant:
+ error_msg = (
+ f"You do not have permission to update app variant: {variant_id}"
+ )
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ db_app_variant = await db_manager.fetch_app_variant_by_id(
+ app_variant_id=variant_id
+ )
+
+ await app_manager.update_variant_image(db_app_variant, image, **user_org_data)
+ except ValueError as e:
+ detail = f"Error while trying to update the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except DockerException as e:
+ detail = f"Docker error while trying to update the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+ except Exception as e:
+ detail = f"Unexpected error while trying to update the app variant: {str(e)}"
+ raise HTTPException(status_code=500, detail=detail)
+
+
+@router.put("/{variant_id}/")
+async def start_variant(
+ request: Request,
+ variant_id: str,
+ action: VariantAction,
+ env_vars: Optional[DockerEnvVars] = None,
+) -> URI:
+ """
+ Start a variant of an app.
+
+ Args:
+ variant_id (str): The ID of the variant to start.
+ action (VariantAction): The action to perform on the variant (start).
+ env_vars (Optional[DockerEnvVars], optional): The environment variables to inject to the Docker container. Defaults to None.
+ stoken_session (SessionContainer, optional): The session container. Defaults to Depends(verify_session()).
+
+ Returns:
+ URI: The URL of the started variant.
+
+ Raises:
+ HTTPException: If the app container cannot be started.
+ """
+
+ logger.debug("Starting variant %s", variant_id)
+ user_org_data: dict = await get_user_and_org_id(request.state.user_id)
+
+ # Inject env vars to docker container
+ if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ if not os.environ["OPENAI_API_KEY"]:
+ raise HTTPException(
+ status_code=400,
+ detail="Unable to start app container. Please file an issue by clicking on the button below.",
+ )
+ envvars = {
+ "OPENAI_API_KEY": os.environ["OPENAI_API_KEY"],
+ }
+ else:
+ envvars = {} if env_vars is None else env_vars.env_vars
+
+ access = await check_access_to_variant(
+ user_org_data=user_org_data, variant_id=variant_id
+ )
+ if not access:
+ error_msg = f"You do not have access to this variant: {variant_id}"
+ logger.error(error_msg)
+ return JSONResponse(
+ {"detail": error_msg},
+ status_code=400,
+ )
+ app_variant_db = await db_manager.fetch_app_variant_by_id(app_variant_id=variant_id)
+ if action.action == VariantActionEnum.START:
+ url: URI = await app_manager.start_variant(
+ app_variant_db, envvars, **user_org_data
+ )
+ return url
diff --git a/agenta-backend/agenta_backend/services/app_manager.py b/agenta-backend/agenta_backend/services/app_manager.py
index 690b3e6609..bad7fb5c55 100644
--- a/agenta-backend/agenta_backend/services/app_manager.py
+++ b/agenta-backend/agenta_backend/services/app_manager.py
@@ -1,104 +1,153 @@
"""Main Business logic
"""
-
+import os
import logging
-from typing import Optional
-from agenta_backend.config import settings
-from agenta_backend.services.db_mongo import testsets
+from urllib.parse import urlparse
+from typing import List, Any, Dict
+
from agenta_backend.models.api.api_models import (
URI,
- App,
- AppVariant,
- Image,
DockerEnvVars,
+ Image,
+)
+from agenta_backend.models.db_models import (
+ AppVariantDB,
+ AppEnvironmentDB,
+ AppDB,
)
-from agenta_backend.services import db_manager, docker_utils
-from docker.errors import DockerException
+from agenta_backend.services import db_manager
+
+if os.environ["FEATURE_FLAG"] in ["cloud"]:
+ from agenta_backend.cloud.services import (
+ lambda_deployment_manager as deployment_manager,
+ ) # noqa pylint: disable-all
+elif os.environ["FEATURE_FLAG"] in ["ee"]:
+ from agenta_backend.ee.services import (
+ deployment_manager,
+ ) # noqa pylint: disable-all
+else:
+ from agenta_backend.services import deployment_manager
+
+if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ from agenta_backend.cloud.services import (
+ api_key_service,
+ ) # noqa pylint: disable-all
-logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
-def _fetch_app_variant_from_db(app_variant: AppVariant) -> Optional[AppVariant]:
+async def start_variant(
+ db_app_variant: AppVariantDB,
+ env_vars: DockerEnvVars = None,
+ **kwargs: dict,
+) -> URI:
"""
- Fetches an app variant from the database.
-
- Args:
- app_variant (AppVariant): The app variant to be fetched.
-
- Returns:
- Optional[AppVariant]: The fetched app variant or None if not found.
-
- Raises:
- Exception: Any exception raised by the db_manager while fetching.
- """
- try:
- return db_manager.get_variant_from_db(app_variant)
- except Exception as e:
- logger.error(f"Error fetching app variant from the database: {str(e)}")
- raise
-
+ Starts a Docker container for a given app variant.
-def _fetch_image_from_db(app_variant: AppVariant) -> Optional[Image]:
- """
- Fetches an image associated with an app variant from the database.
+ Fetches the associated image from the database and delegates to a Docker utility function
+ to start the container. The URI of the started container is returned.
Args:
- app_variant (AppVariant): The app variant whose associated image is to be fetched.
+ app_variant (AppVariant): The app variant for which a container is to be started.
+ env_vars (DockerEnvVars): (optional) The environment variables to be passed to the container.
Returns:
- Optional[Image]: The fetched image or None if not found.
-
- Raises:
- Exception: Any exception raised by the db_manager while fetching.
- """
- try:
- return db_manager.get_image(app_variant)
- except Exception as e:
- logger.error(f"Error fetching image from the database: {str(e)}")
- return None
-
-
-def _stop_and_delete_containers(image: Image) -> None:
- """
- Stops and deletes Docker containers associated with a given image.
-
- Args:
- image (Image): The Docker image whose associated containers are to be stopped and deleted.
+ URI: The URI of the started Docker container.
Raises:
- Exception: Any exception raised during Docker operations.
+ ValueError: If the app variant does not have a corresponding image in the database.
+ RuntimeError: If there is an error starting the Docker container.
"""
try:
- container_ids = docker_utils.stop_containers_based_on_image(image)
- logger.info(f"Containers {container_ids} stopped")
- for container_id in container_ids:
- docker_utils.delete_container(container_id)
- logger.info(f"Container {container_id} deleted")
+ logger.debug(
+ "Starting variant %s with image name %s and tags %s and app_name %s and organization %s",
+ db_app_variant.variant_name,
+ db_app_variant.image.docker_id,
+ db_app_variant.image.tags,
+ db_app_variant.app.app_name,
+ db_app_variant.organization,
+ )
+ logger.debug("App name is %s", db_app_variant.app.app_name)
+ # update the env variables
+ domain_name = os.environ.get("DOMAIN_NAME")
+ if domain_name is None or domain_name == "http://localhost":
+ # in the case of agenta running locally, the containers can access the host machine via this address
+ domain_name = (
+ "http://host.docker.internal" # unclear why this stopped working
+ )
+ # domain_name = "http://localhost"
+ env_vars = {} if env_vars is None else env_vars
+ env_vars.update(
+ {"AGENTA_BASE_ID": str(db_app_variant.base.id), "AGENTA_HOST": domain_name}
+ )
+ if os.environ["FEATURE_FLAG"] in ["cloud", "ee"]:
+ api_key = await api_key_service.create_api_key(
+ str(db_app_variant.user.uid), expiration_date=None, hidden=True
+ )
+ env_vars.update({"AGENTA_API_KEY": api_key})
+ deployment = await deployment_manager.start_service(
+ app_variant_db=db_app_variant, env_vars=env_vars
+ )
+ await db_manager.update_base(
+ db_app_variant.base,
+ deployment=deployment.id,
+ )
except Exception as e:
- logger.error(f"Error stopping and deleting Docker containers: {str(e)}")
+ logger.error(
+ f"Error starting Docker container for app variant {db_app_variant.app.app_name}/{db_app_variant.variant_name}: {str(e)}"
+ )
+ raise Exception(
+ f"Failed to start Docker container for app variant {db_app_variant.app.app_name}/{db_app_variant.variant_name} \n {str(e)}"
+ ) from e
+ return URI(uri=deployment.uri)
-def _delete_docker_image(image: Image) -> None:
- """
- Deletes a Docker image.
- Args:
- image (Image): The Docker image to be deleted.
+async def update_variant_image(
+ app_variant_db: AppVariantDB, image: Image, **kwargs: dict
+):
+ """Updates the image for app variant in the database.
- Raises:
- Exception: Any exception raised during Docker operations.
+ Arguments:
+ app_variant -- the app variant to update
+ image -- the image to update
"""
- try:
- docker_utils.delete_image(image)
- logger.info(f"Image {image.tags} deleted")
- except Exception as e:
- logger.warning(
- f"Warning: Error deleting image {image.tags}. Probably multiple variants using it."
- )
-
-def remove_app_variant(app_variant: AppVariant) -> None:
+ valid_image = await deployment_manager.validate_image(image)
+ if not valid_image:
+ raise ValueError("Image could not be found in registry.")
+ deployment = await db_manager.get_deployment_by_objectid(
+ app_variant_db.base.deployment
+ )
+
+ await deployment_manager.stop_and_delete_service(deployment)
+ await db_manager.remove_deployment(deployment)
+
+ if os.environ["FEATURE_FLAG"] in ["ee", "oss"]:
+ await deployment_manager.remove_image(app_variant_db.base.image)
+
+ await db_manager.remove_image(app_variant_db.base.image)
+ # Create a new image instance
+ db_image = await db_manager.create_image(
+ image_type="image",
+ tags=image.tags,
+ docker_id=image.docker_id,
+ user=app_variant_db.user,
+ deletable=True,
+ organization=app_variant_db.organization,
+ )
+ # Update base with new image
+ await db_manager.update_base(app_variant_db.base, image=db_image)
+ # Update variant with new image
+ app_variant_db = await db_manager.update_app_variant(app_variant_db, image=db_image)
+ # Start variant
+ await start_variant(app_variant_db, **kwargs)
+
+
+async def terminate_and_remove_app_variant(
+ app_variant_id: str = None, app_variant_db=None, **kwargs: dict
+) -> None:
"""
Removes app variant from the database. If it's the last one using an image, performs additional operations:
- Deletes the image from the db.
@@ -106,36 +155,129 @@ def remove_app_variant(app_variant: AppVariant) -> None:
- Removes the image from the registry.
Args:
- app_variant (AppVariant): The app variant to remove.
+ variant_id (srt): The app variant to remove.
Raises:
ValueError: If the app variant is not found in the database.
Exception: Any other exception raised during the operation.
"""
- app_variant_db = _fetch_app_variant_from_db(app_variant)
-
+ assert (
+ app_variant_id or app_variant_db
+ ), "Either app_variant_id or app_variant_db must be provided"
+ assert not (
+ app_variant_id and app_variant_db
+ ), "Only one of app_variant_id or app_variant_db must be provided"
+
+ logger.debug(f"Removing app variant {app_variant_id}")
+ if app_variant_id:
+ app_variant_db = await db_manager.fetch_app_variant_by_id(app_variant_id)
+
+ logger.debug(f"Fetched app variant {app_variant_db}")
+ app_id = app_variant_db.app.id
if app_variant_db is None:
- msg = f"App variant {app_variant.app_name}/{app_variant.variant_name} not found in DB"
- logger.error(msg)
- raise ValueError(msg)
+ error_msg = f"Failed to delete app variant {app_variant_id}: Not found in DB."
+ logger.error(error_msg)
+ raise ValueError(error_msg)
try:
- if db_manager.check_is_last_variant(app_variant_db):
- image = _fetch_image_from_db(app_variant)
- print("we reached here")
+ is_last_variant_for_image = await db_manager.check_is_last_variant_for_image(
+ app_variant_db
+ )
+ if is_last_variant_for_image:
+ # remove variant + terminate and rm containers + remove base
+
+ image = app_variant_db.base.image
+ logger.debug("is_last_variant_for_image {image}")
if image:
- _stop_and_delete_containers(image)
- _delete_docker_image(image)
- db_manager.remove_image(image)
+ logger.debug("_stop_and_delete_app_container")
+ try:
+ deployment = await db_manager.get_deployment_by_objectid(
+ app_variant_db.base.deployment
+ )
+ except Exception as e:
+ logger.error(f"Failed to get deployment {e}")
+ deployment = None
+ if deployment:
+ try:
+ await deployment_manager.stop_and_delete_service(deployment)
+ except RuntimeError as e:
+ logger.error(
+ f"Failed to stop and delete service {deployment} {e}"
+ )
+
+ # If image deletable is True, remove docker image and image db
+ if image.deletable:
+ try:
+ if os.environ["FEATURE_FLAG"] == "cloud":
+ await deployment_manager.remove_repository(image.tags)
+ else:
+ await deployment_manager.remove_image(image)
+ except RuntimeError as e:
+ logger.error(f"Failed to remove image {image} {e}")
+ await db_manager.remove_image(image, **kwargs)
+
+ logger.debug("remove base")
+ await db_manager.remove_app_variant_from_db(app_variant_db, **kwargs)
+ logger.debug("Remove image object from db")
+ if deployment:
+ await db_manager.remove_deployment(deployment)
+ await db_manager.remove_base_from_db(app_variant_db.base, **kwargs)
+ logger.debug("remove_app_variant_from_db")
+
+ # Only delete the docker image for users that are running the oss version
+
+ else:
+ logger.debug(
+ f"Image associated with app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name} not found. Skipping deletion."
+ )
+ else:
+ # remove variant + config
+ logger.debug("remove_app_variant_from_db")
+ await db_manager.remove_app_variant_from_db(app_variant_db, **kwargs)
+ logger.debug("list_app_variants")
+ app_variants = await db_manager.list_app_variants(app_id=app_id, **kwargs)
+ logger.debug(f"{app_variants}")
+ if len(app_variants) == 0: # this was the last variant for an app
+ logger.debug("remove_app_related_resources")
+ await remove_app_related_resources(app_id=app_id, **kwargs)
+ except Exception as e:
+ logger.error(
+ f"An error occurred while deleting app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name}: {str(e)}"
+ )
+ raise e from None
- db_manager.remove_app_variant(app_variant)
+async def remove_app_related_resources(app_id: str, **kwargs: dict):
+ """Removes environments and testsets associated with an app after its deletion.
+
+ When an app or its last variant is deleted, this function ensures that
+ all related resources such as environments and testsets are also deleted.
+
+ Args:
+ app_name: The name of the app whose associated resources are to be removed.
+ """
+ try:
+ # Delete associated environments
+ environments: List[AppEnvironmentDB] = await db_manager.list_environments(
+ app_id, **kwargs
+ )
+ for environment_db in environments:
+ await db_manager.remove_environment(environment_db, **kwargs)
+ logger.info(f"Successfully deleted environment {environment_db.name}.")
+ # Delete associated testsets
+ await db_manager.remove_app_testsets(app_id, **kwargs)
+ logger.info(f"Successfully deleted test sets associated with app {app_id}.")
+
+ await db_manager.remove_app_by_id(app_id, **kwargs)
+ logger.info(f"Successfully remove app object {app_id}.")
except Exception as e:
- logger.error(f"Error deleting app variant: {str(e)}")
- raise
+ logger.error(
+ f"An error occurred while cleaning up resources for app {app_id}: {str(e)}"
+ )
+ raise e from None
-async def remove_app(app: App):
+async def remove_app(app_id: str, **kwargs: dict):
"""Removes all app variants from db, if it is the last one using an image, then
deletes the image from the db, shutdowns the container, deletes it and remove
the image from the registry
@@ -144,199 +286,186 @@ async def remove_app(app: App):
app_name -- the app name to remove
"""
# checks if it is the last app variant using its image
- app_name = app.app_name
- if app_name not in [app.app_name for app in db_manager.list_apps()]:
- msg = f"App {app_name} not found in DB"
- logger.error(msg)
- raise ValueError(msg)
+ app = await db_manager.fetch_app_by_id(app_id)
+ if app is None:
+ error_msg = f"Failed to delete app {app_id}: Not found in DB."
+ logger.error(error_msg)
+ raise ValueError(error_msg)
try:
- app_variants = db_manager.list_app_variants(
- app_name=app_name, show_soft_deleted=True
- )
- except Exception as e:
- logger.error(f"Error fetching app variants from the database: {str(e)}")
- raise
- if app_variants is None:
- msg = f"App {app_name} not found in DB"
- logger.error(msg)
- raise ValueError(msg)
- else:
- try:
- for app_variant in app_variants:
- remove_app_variant(app_variant)
- logger.info(
- f"App variant {app_variant.app_name}/{app_variant.variant_name} deleted"
- )
-
- await remove_app_testsets(app_name)
- if app_variant.variant_name == "v1":
- docker_utils.stop_container(f"{app_variant.app_name}-v1")
- docker_utils.delete_container(f"{app_variant.app_name}-v1")
- logger.info(f"Tatasets for {app_name} app deleted")
- except Exception as e:
- logger.error(f"Error deleting app variants: {str(e)}")
- raise
-
-
-async def remove_app_testsets(app_name: str):
- """Returns a list of testsets owned by an app.
-
- Args:
- app_name (str): The name of the app
-
- Returns:
- int: The number of testsets deleted
- """
-
- # Find testsets owned by the app
- cursor = testsets.find({"app_name": app_name})
- documents = await cursor.to_list(length=100)
-
- # Prepare a list of ObjectIds for bulk deletion
- testset_ids = [document["_id"] for document in documents]
-
- # Perform bulk deletion if there are testsets to delete
- if testset_ids:
- result = await testsets.delete_many({"_id": {"$in": testset_ids}})
- deleted_count = result.deleted_count
- logger.info(f"{deleted_count} testset(s) deleted for app {app_name}")
- return deleted_count
- else:
- logger.info(f"No testsets found for app {app_name}")
- return 0
-
-
-def start_variant(app_variant: AppVariant, env_vars: DockerEnvVars = None) -> URI:
- """
- Starts a Docker container for a given app variant.
-
- Fetches the associated image from the database and delegates to a Docker utility function
- to start the container. The URI of the started container is returned.
-
- Args:
- app_variant (AppVariant): The app variant for which a container is to be started.
- env_vars (DockerEnvVars): (optional) The environment variables to be passed to the container.
-
- Returns:
- URI: The URI of the started Docker container.
-
- Raises:
- ValueError: If the app variant does not have a corresponding image in the database.
- RuntimeError: If there is an error starting the Docker container.
- """
- try:
- image: Image = db_manager.get_image(app_variant)
- except Exception as e:
- logger.error(
- f"Error fetching image for app variant {app_variant.app_name}/{app_variant.variant_name} from database: {str(e)}"
- )
- raise Exception(
- f"Image for app variant {app_variant.app_name}/{app_variant.variant_name} not found in database \n {str(e)}"
- )
+ app_variants = await db_manager.list_app_variants(app_id=app_id, **kwargs)
+ for app_variant_db in app_variants:
+ await terminate_and_remove_app_variant(
+ app_variant_db=app_variant_db, **kwargs
+ )
+ logger.info(
+ f"Successfully deleted app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name}."
+ )
+
+ if len(app_variants) == 0: # Failsafe in case something went wrong before
+ logger.debug("remove_app_related_resources")
+ await remove_app_related_resources(app_id=app_id, **kwargs)
- try:
- uri: URI = docker_utils.start_container(
- image_name=image.tags,
- app_name=app_variant.app_name,
- variant_name=app_variant.variant_name,
- env_vars=env_vars,
- )
- logger.info(
- f"Started Docker container for app variant {app_variant.app_name}/{app_variant.variant_name} at URI {uri}"
- )
except Exception as e:
logger.error(
- f"Error starting Docker container for app variant {app_variant.app_name}/{app_variant.variant_name}: {str(e)}"
- )
- raise Exception(
- f"Failed to start Docker container for app variant {app_variant.app_name}/{app_variant.variant_name} \n {str(e)}"
+ f"An error occurred while deleting app {app_id} and its associated resources: {str(e)}"
)
+ raise e from None
- return uri
-
-def update_variant_parameters(app_variant: AppVariant):
+async def update_variant_parameters(
+ app_variant_id: str, parameters: Dict[str, Any], **kwargs: dict
+):
"""Updates the parameters for app variant in the database.
Arguments:
app_variant -- the app variant to update
"""
- if app_variant.app_name in ["", None] or app_variant.variant_name == [
- "",
- None,
- ]:
- msg = f"App name and variant name cannot be empty"
- logger.error(msg)
- raise ValueError(msg)
- if app_variant.parameters is None:
- msg = f"Parameters cannot be empty when updating app variant"
- logger.error(msg)
- raise ValueError(msg)
+ assert app_variant_id is not None, "app_variant_id must be provided"
+ assert parameters is not None, "parameters must be provided"
+ app_variant_db = await db_manager.fetch_app_variant_by_id(app_variant_id)
+ if app_variant_db is None:
+ error_msg = f"Failed to update app variant {app_variant_id}: Not found in DB."
+ logger.error(error_msg)
+ raise ValueError(error_msg)
try:
- db_manager.update_variant_parameters(app_variant, app_variant.parameters)
- except:
+ await db_manager.update_variant_parameters(
+ app_variant_db=app_variant_db, parameters=parameters, **kwargs
+ )
+ except Exception as e:
logger.error(
- f"Error updating app variant {app_variant.app_name}/{app_variant.variant_name}"
+ f"Error updating app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name}"
)
- raise
+ raise e from None
+
+
+async def add_variant_based_on_image(
+ app: AppDB,
+ variant_name: str,
+ docker_id_or_template_uri: str,
+ tags: str = None,
+ base_name: str = None,
+ config_name: str = "default",
+ is_template_image: bool = False,
+ **user_org_data: dict,
+) -> AppVariantDB:
+ """
+ Adds a new variant to the app based on the specified Docker image.
+ Args:
+ app (AppDB): The app to add the variant to.
+ variant_name (str): The name of the new variant.
+ docker_id (str): The ID of the Docker image to use for the new variant.
+ tags (str): The tags associated with the Docker image.
+ base_name (str, optional): The name of the base to use for the new variant. Defaults to None.
+ config_name (str, optional): The name of the configuration to use for the new variant. Defaults to "default".
+ is_template_image (bool, optional): Whether or not the image used is for a template (in this case we won't delete it in the future).
+ **user_org_data (dict): Additional user and organization data.
-def update_variant_image(app_variant: AppVariant, image: Image):
- """Updates the image for app variant in the database.
+ Returns:
+ AppVariantDB: The newly created app variant.
- Arguments:
- app_variant -- the app variant to update
- image -- the image to update
+ Raises:
+ ValueError: If the app variant or image is None, or if an app variant with the same name already exists.
+ HTTPException: If an error occurs while creating the app variant.
"""
- if app_variant.app_name in ["", None] or app_variant.variant_name == [
- "",
- None,
- ]:
- msg = "App name and variant name cannot be empty"
- logger.error(msg)
- raise ValueError(msg)
- if image.tags in ["", None]:
- msg = "Image tags cannot be empty"
- logger.error(msg)
- raise ValueError(msg)
- if not image.tags.startswith(settings.registry):
- raise ValueError(
- "Image should have a tag starting with the registry name (agenta-server)"
+ logger.debug("Start: Creating app variant based on image")
+
+ # Validate input parameters
+ logger.debug("Step 1: Validating input parameters")
+ if (
+ app in [None, ""]
+ or variant_name in [None, ""]
+ or docker_id_or_template_uri in [None, ""]
+ ):
+ raise ValueError("App variant or image is None")
+
+ if os.environ["FEATURE_FLAG"] not in ["cloud"]:
+ if tags in [None, ""]:
+ raise ValueError("OSS: Tags is None")
+
+ db_image = None
+ # Check if docker_id_or_template_uri is a URL or not
+ parsed_url = urlparse(docker_id_or_template_uri)
+
+ # Check if app variant already exists
+ logger.debug("Step 2: Checking if app variant already exists")
+ variants = await db_manager.list_app_variants_for_app_id(
+ app_id=str(app.id), **user_org_data
+ )
+ already_exists = any([av for av in variants if av.variant_name == variant_name])
+ if already_exists:
+ logger.error("App variant with the same name already exists")
+ raise ValueError("App variant with the same name already exists")
+
+ # Retrieve user and image objects
+ logger.debug("Step 3: Retrieving user and image objects")
+ user_instance = await db_manager.get_user(user_uid=user_org_data["uid"])
+ if parsed_url.scheme and parsed_url.netloc:
+ db_image = await db_manager.get_orga_image_instance_by_uri(
+ organization_id=str(app.organization.id),
+ template_uri=docker_id_or_template_uri,
)
- if image not in docker_utils.list_images():
- raise DockerException(
- f"Image {image.docker_id} with tags {image.tags} not found"
+ else:
+ db_image = await db_manager.get_orga_image_instance_by_docker_id(
+ organization_id=str(app.organization.id),
+ docker_id=docker_id_or_template_uri,
)
- if db_manager.get_variant_from_db(app_variant) is None:
- msg = f"App variant {app_variant.app_name}/{app_variant.variant_name} not found in DB"
- logger.error(msg)
- raise ValueError(msg)
- try:
- old_variant = db_manager.get_variant_from_db(app_variant)
- old_image = db_manager.get_image(old_variant)
- container_ids = docker_utils.stop_containers_based_on_image(old_image)
- logger.info(f"Containers {container_ids} stopped")
- for container_id in container_ids:
- docker_utils.delete_container(container_id)
- logger.info(f"Container {container_id} deleted")
- db_manager.remove_app_variant(old_variant)
- except Exception as e:
- logger.error(f"Error removing old variant: {str(e)}")
- logger.error(
- f"Error removing and shutting down containers for old app variant {app_variant.app_name}/{app_variant.variant_name}"
- )
- logger.error("Previous variant removed but new variant not added. Rolling back")
- db_manager.add_variant_based_on_image(old_variant, old_image)
- raise
- try:
- logger.info(
- f"Updating variant {app_variant.app_name}/{app_variant.variant_name}"
- )
- db_manager.add_variant_based_on_image(app_variant, image)
- logger.info(
- f"Starting variant {app_variant.app_name}/{app_variant.variant_name}"
- )
- start_variant(app_variant)
- except:
- raise
+ # Create new image if not exists
+ if db_image is None:
+ logger.debug("Step 4: Creating new image")
+ if parsed_url.scheme and parsed_url.netloc:
+ db_image = await db_manager.create_image(
+ image_type="zip",
+ template_uri=docker_id_or_template_uri,
+ deletable=not (is_template_image),
+ user=user_instance,
+ organization=app.organization,
+ )
+ else:
+ docker_id = docker_id_or_template_uri
+ db_image = await db_manager.create_image(
+ image_type="image",
+ docker_id=docker_id,
+ tags=tags,
+ deletable=not (is_template_image),
+ user=user_instance,
+ organization=app.organization,
+ )
+
+ # Create config
+ logger.debug("Step 5: Creating config")
+ config_db = await db_manager.create_new_config(
+ config_name=config_name, parameters={}
+ )
+
+ # Create base
+ logger.debug("Step 6: Creating base")
+ if not base_name:
+ base_name = variant_name.split(".")[
+ 0
+ ] # TODO: Change this in SDK2 to directly use base_name
+ db_base = await db_manager.create_new_variant_base(
+ app=app,
+ organization=app.organization,
+ user=user_instance,
+ base_name=base_name, # the first variant always has default base
+ image=db_image,
+ )
+
+ # Create app variant
+ logger.debug("Step 7: Creating app variant")
+ db_app_variant = await db_manager.create_new_app_variant(
+ app=app,
+ variant_name=variant_name,
+ image=db_image,
+ user=user_instance,
+ organization=app.organization,
+ parameters={},
+ base_name=base_name,
+ config_name=config_name,
+ base=db_base,
+ config=config_db,
+ )
+ logger.debug("End: Successfully created db_app_variant: %s", db_app_variant)
+ return db_app_variant
diff --git a/agenta-backend/agenta_backend/services/auth_helper.py b/agenta-backend/agenta_backend/services/auth_helper.py
new file mode 100644
index 0000000000..263de7c4e6
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/auth_helper.py
@@ -0,0 +1,31 @@
+from fastapi import Request, HTTPException
+
+
+class SessionContainer(object):
+ """dummy class"""
+
+ pass
+
+
+def verify_session():
+ """dummy function"""
+
+ def inner_function():
+ """dummy function"""
+ return SessionContainer()
+
+ return inner_function
+
+
+async def authentication_middleware(request: Request, call_next):
+ try:
+ # Initialize the user_id attribute in the request state if it doesn't exist
+ if not hasattr(request.state, "user_id"):
+ user_uid_id = "0"
+ setattr(request.state, "user_id", user_uid_id)
+
+ # Call the next middleware or route handler
+ response = await call_next(request)
+ return response
+ except Exception as e:
+ raise HTTPException(status_code=401, detail=str(e))
diff --git a/agenta-backend/agenta_backend/services/cache_manager.py b/agenta-backend/agenta_backend/services/cache_manager.py
deleted file mode 100644
index 5d68c5d7ba..0000000000
--- a/agenta-backend/agenta_backend/services/cache_manager.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import json
-import redis
-from typing import List
-from agenta_backend.config import settings
-from agenta_backend.services.container_manager import (
- retrieve_templates_from_dockerhub,
- get_templates_info,
-)
-
-
-def redis_connection() -> redis.Redis:
- """Returns a Redis client object connected to a Redis server specified
- by the `redis_url` setting.
-
- Returns:
- A Redis client object.
- """
-
- redis_client = redis.from_url(url=settings.redis_url)
- return redis_client
-
-
-async def retrieve_templates_from_dockerhub_cached(cache: bool) -> List[dict]:
- """Retrieves templates from Docker Hub and caches the data in Redis for future use.
- Args:
- cache: A boolean value that indicates whether to use the cached data or not.
- Returns:
- List of tags data (cached or network-call)
- """
- r = redis_connection()
- if cache:
- cached_data = r.get("templates_data")
- if cached_data is not None:
- return json.loads(cached_data.decode("utf-8"))
-
- # If not cached, fetch data from Docker Hub and cache it in Redis
- response = await retrieve_templates_from_dockerhub(
- settings.docker_hub_url,
- settings.docker_hub_repo_owner,
- settings.docker_hub_repo_name,
- )
- response_data = response["results"]
-
- # Cache the data in Redis for 60 minutes
- r.set("templates_data", json.dumps(response_data), ex=900)
- return response_data
-
-
-async def retrieve_templates_info_from_dockerhub_cached(cache: bool) -> List[dict]:
- """Retrieves templates information from Docker Hub and caches the data in Redis for future use.
- Args:
- cache: A boolean value that indicates whether to use the cached data or not.
- Returns:
- Information about organization in DockerHub (cached or network-call)
- """
- r = redis_connection()
- if cache:
- cached_data = r.get("org_data")
- if cached_data is not None:
- print("Using cache...")
- return json.loads(cached_data.decode("utf-8"))
-
- # If not cached, fetch data from Docker Hub and cache it in Redis
- response = await get_templates_info(
- settings.docker_hub_url,
- settings.docker_hub_repo_owner,
- settings.docker_hub_repo_name,
- )
- response_data = response["full_description"]
-
- # Cache the data in Redis for 60 minutes
- r.set("org_data", json.dumps(response_data), ex=900)
- print("Using network call...")
- return response_data
diff --git a/agenta-backend/agenta_backend/services/container_manager.py b/agenta-backend/agenta_backend/services/container_manager.py
index 6f4d025b3f..5ee1418703 100644
--- a/agenta-backend/agenta_backend/services/container_manager.py
+++ b/agenta-backend/agenta_backend/services/container_manager.py
@@ -1,17 +1,25 @@
-import httpx
-import shutil
-import docker
+import asyncio
import logging
-import backoff
+import os
+import shutil
+import uuid
+from asyncio.exceptions import CancelledError
+from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
-from aiodocker import Docker
-from httpx import ConnectError
-from typing import List, Union
-from fastapi import HTTPException
+from typing import Any, Dict, List, Union
-from asyncio.exceptions import CancelledError
-from agenta_backend.models.api.api_models import Image
+import backoff
+import docker
+import httpx
+from aiodocker import Docker, exceptions
+from fastapi import HTTPException, UploadFile
+from httpx import ConnectError, TimeoutException
+from agenta_backend.models.api.api_models import Image
+from agenta_backend.models.db_models import (
+ AppDB,
+)
+from agenta_backend.services import docker_utils
client = docker.from_env()
@@ -20,9 +28,45 @@
logger.setLevel(logging.INFO)
+async def build_image(app_db: AppDB, base_name: str, tar_file: UploadFile) -> Image:
+ app_name = app_db.app_name
+ organization_id = str(app_db.organization.id)
+
+ image_name = f"agentaai/{app_name.lower()}_{base_name.lower()}:latest"
+ # Get event loop
+ loop = asyncio.get_event_loop()
+
+ # Create a ThreadPoolExecutor for running threads
+ thread_pool = ThreadPoolExecutor(max_workers=4)
+
+ # Create a unique temporary directory for each upload
+ temp_dir = Path(f"/tmp/{uuid.uuid4()}")
+ temp_dir.mkdir(parents=True, exist_ok=True)
+
+ # Save uploaded file to the temporary directory
+ tar_path = temp_dir / tar_file.filename
+ with tar_path.open("wb") as buffer:
+ buffer.write(await tar_file.read())
+ future = loop.run_in_executor(
+ thread_pool,
+ build_image_job,
+ *(
+ app_name,
+ base_name,
+ organization_id,
+ tar_path,
+ image_name,
+ temp_dir,
+ ),
+ )
+ image_result = await asyncio.wrap_future(future)
+ return image_result
+
+
def build_image_job(
app_name: str,
- variant_name: str,
+ base_name: str,
+ organization_id: str,
tar_path: Path,
image_name: str,
temp_dir: Path,
@@ -33,9 +77,10 @@ def build_image_job(
Arguments:
app_name -- The `app_name` parameter is a string that represents the name of the application
- variant_name -- The `variant_name` parameter is a string that represents the variant of the \
+ base_name -- The `base_name` parameter is a string that represents the variant of the \
application. It could be a specific version, configuration, or any other distinguishing \
factor for the application
+ organization_id -- The id of the organization the app belongs to
tar_path -- The `tar_path` parameter is the path to the tar file that contains the source code \
or files needed to build the Docker image
image_name -- The `image_name` parameter is a string that represents the name of the Docker \
@@ -55,15 +100,26 @@ def build_image_job(
shutil.unpack_archive(tar_path, temp_dir)
try:
+ if os.environ["FEATURE_FLAG"] in ["cloud"]:
+ dockerfile = "Dockerfile.cloud"
+ else:
+ dockerfile = "Dockerfile"
image, build_log = client.images.build(
path=str(temp_dir),
tag=image_name,
- buildargs={"ROOT_PATH": f"/{app_name}/{variant_name}"},
+ buildargs={"ROOT_PATH": f"/{organization_id}/{app_name}/{base_name}"},
rm=True,
+ dockerfile=dockerfile,
+ pull=True,
)
for line in build_log:
logger.info(line)
- return Image(docker_id=image.id, tags=image.tags[0])
+ return Image(
+ type="image",
+ docker_id=image.id,
+ tags=image.tags[0],
+ organization_id=organization_id,
+ )
except docker.errors.BuildError as ex:
log = "Error building Docker image:\n"
log += str(ex) + "\n"
@@ -73,60 +129,6 @@ def build_image_job(
raise HTTPException(status_code=500, detail=str(ex))
-@backoff.on_exception(backoff.expo, (ConnectError, CancelledError), max_tries=5)
-async def retrieve_templates_from_dockerhub(
- url: str, repo_owner: str, repo_name: str
-) -> Union[List[dict], dict]:
- """
- Business logic to retrieve templates from DockerHub.
-
- Args:
- url (str): The URL endpoint for retrieving templates. Should contain placeholders `{}`
- for the `repo_owner` and `repo_name` values to be inserted. For example:
- `https://hub.docker.com/v2/repositories/{}/{}/tags`.
- repo_owner (str): The owner or organization of the repository from which templates are to be retrieved.
- repo_name (str): The name of the repository where the templates are located.
-
- Returns:
- tuple: A tuple containing two values.
- """
-
- async with httpx.AsyncClient() as client:
- response = await client.get(
- f"{url.format(repo_owner, repo_name)}/tags", timeout=10
- )
- if response.status_code == 200:
- response_data = response.json()
- return response_data
-
- response_data = response.json()
- return response_data
-
-
-async def get_templates_info(url: str, repo_owner: str, repo_name: str) -> dict:
- """
- Business logic to retrieve templates from DockerHub.
-
- Args:
- url (str): The URL endpoint for retrieving templates. Should contain placeholders `{}`
- for the `repo_owner` and `repo_name` values to be inserted. For example:
- `https://hub.docker.com/v2/repositories/{}/{}/tags`.
- repo_owner (str): The owner or organization of the repository from which templates are to be retrieved.
- repo_name (str): The name of the repository where the templates are located.
-
- Returns:
- tuple: A tuple containing two values.
- """
- async with httpx.AsyncClient() as client:
- response = await client.get(f"{url.format(repo_owner, repo_name)}/", timeout=10)
- if response.status_code == 200:
- response_data = response.json()
- return response_data
-
- response_data = response.json()
- return response_data
-
-
async def check_docker_arch() -> str:
"""Checks the architecture of the Docker system.
@@ -148,17 +150,23 @@ async def check_docker_arch() -> str:
return arch_mapping.get(info["Architecture"], "unknown")
-async def pull_image_from_docker_hub(repo_name: str, tag: str) -> dict:
- """Bussiness logic to asynchronously pull an image from Docker Hub.
+@backoff.on_exception(
+ backoff.expo,
+ (ConnectError, TimeoutException, CancelledError, exceptions.DockerError),
+ max_tries=5,
+)
+async def pull_docker_image(repo_name: str, tag: str) -> dict:
+ """Business logic to asynchronously pull an image from either Docker Hub or ECR.
Args:
- repo_name (str): The name of the repository on Docker Hub from which the image is to be pulled.
+ repo_name (str): The name of the repository from which the image is to be pulled.
Typically follows the format `username/repository_name`.
- tag (str): Specifies a specific version or tag of the image to pull from the Docker Hub repository.
+ tag (str): Specifies a specific version or tag of the image to pull from the repository.
Returns:
- Image: An image object from Docker Hub.
+ Image: An image object.
"""
+
async with Docker() as docker:
image = await docker.images.pull(repo_name, tag=tag)
return image
@@ -182,3 +190,12 @@ async def get_image_details_from_docker_hub(
f"{repo_owner}/{repo_name}:{image_name}"
)
return image_details["Id"]
+
+
+def restart_container(container_id: str):
+ """Restart docker container.
+
+ Args:
+ container_id (str): The id of the container to restart.
+ """
+ docker_utils.restart_container(container_id)
diff --git a/agenta-backend/agenta_backend/services/db_manager.py b/agenta-backend/agenta_backend/services/db_manager.py
index d42febe3a2..9df8b95fb9 100644
--- a/agenta-backend/agenta_backend/services/db_manager.py
+++ b/agenta-backend/agenta_backend/services/db_manager.py
@@ -1,463 +1,1599 @@
import os
-from typing import Dict, List, Optional, Any
+import logging
+from pathlib import Path
+from bson import ObjectId
+from datetime import datetime
+from urllib.parse import urlparse
+from typing import Any, Dict, List, Optional
from agenta_backend.models.api.api_models import (
App,
AppVariant,
- Image,
+ ImageExtended,
Template,
)
from agenta_backend.models.converters import (
- app_variant_db_to_pydantic,
+ app_db_to_pydantic,
image_db_to_pydantic,
templates_db_to_pydantic,
)
-from agenta_backend.models.db_models import AppVariantDB, ImageDB, TemplateDB
-from agenta_backend.services import helpers
-from sqlmodel import Session, SQLModel, create_engine, func, and_
-import logging
+from agenta_backend.services.json_importer_helper import get_json
+from agenta_backend.models.db_models import (
+ AppDB,
+ AppVariantDB,
+ VariantBaseDB,
+ ConfigDB,
+ ConfigVersionDB,
+ AppEnvironmentDB,
+ EvaluationDB,
+ EvaluationScenarioDB,
+ ImageDB,
+ OrganizationDB,
+ TemplateDB,
+ TestSetDB,
+ UserDB,
+)
+
+from agenta_backend.utils.common import check_user_org_access, engine
-# SQLite database connection
-DATABASE_URL = os.environ["DATABASE_URL"]
-engine = create_engine(DATABASE_URL)
-# Create tables if they don't exist
-SQLModel.metadata.create_all(engine)
+from agenta_backend.models.db_models import DeploymentDB
+from fastapi import HTTPException
+from fastapi.responses import JSONResponse
+
+from odmantic import query
+from odmantic.exceptions import DocumentParsingError
+
+
+# Define logger
logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
+logger.setLevel(logging.DEBUG)
+# Define parent directory
+PARENT_DIRECTORY = Path(os.path.dirname(__file__)).parent
-def get_session():
- """Returns a session to the database
- Yields:
- SQLModel.Session: A session to the database
+async def add_testset_to_app_variant(
+ app_id: str, org_id: str, template_name: str, app_name: str, **kwargs: dict
+):
+ """Add testset to app variant.
+ Args:
+ app_id (str): The id of the app
+ org_id (str): The id of the organization
+ template_name (str): The name of the app template image
+ app_name (str): The name of the app
+ **kwargs (dict): Additional keyword arguments
"""
- with Session(engine) as session:
- yield session
+ app_db = await get_app_instance_by_id(app_id)
+ org_db = await get_organization_object(org_id)
+ user_db = await get_user(user_uid=kwargs["uid"])
-def get_templates() -> List[Template]:
- with Session(engine) as session:
- templates = session.query(TemplateDB).all()
- return templates_db_to_pydantic(templates)
+ if template_name == "single_prompt":
+ json_path = (
+ f"{PARENT_DIRECTORY}/resources/default_testsets/single_prompt_testsets.json"
+ )
+ csvdata = get_json(json_path)
+ testset = {
+ "name": f"{app_name}_testset",
+ "app_name": app_name,
+ "created_at": datetime.now().isoformat(),
+ "csvdata": csvdata,
+ }
+ testset = TestSetDB(**testset, app=app_db, user=user_db, organization=org_db)
+ await engine.save(testset)
+
+
+async def get_image(app_variant: AppVariant, **kwargs: dict) -> ImageExtended:
+ """Returns the image associated with the app variant
+
+ Arguments:
+ app_variant -- AppVariant to fetch the image for
+
+ Returns:
+ Image -- The Image associated with the app variant
+ """
+ # Build the query expression for the two conditions
+ query_expression = (
+ query.eq(AppVariantDB.app, ObjectId(app_variant.app_id))
+ & query.eq(AppVariantDB.variant_name, app_variant.variant_name)
+ & query.eq(AppVariantDB.organization, ObjectId(app_variant.organization))
+ )
-def add_template(**kwargs: dict):
- with Session(engine) as session:
- existing_template = (
- session.query(TemplateDB)
- .filter_by(template_id=kwargs["template_id"])
- .first()
+ db_app_variant: AppVariantDB = await engine.find_one(AppVariantDB, query_expression)
+ if db_app_variant:
+ image_db: ImageDB = await engine.find_one(
+ ImageDB, ImageDB.id == ObjectId(db_app_variant.image.id)
)
- if existing_template:
- pass
- else:
- db_template = TemplateDB(**kwargs)
- session.add(db_template)
- session.commit()
- session.refresh(db_template)
+ return image_db_to_pydantic(image_db)
+ else:
+ raise Exception("App variant not found")
-def add_variant_based_on_image(app_variant: AppVariant, image: Image):
- """Adds an app variant based on an image. This the functionality called by the cli.
- Currently we are not using the parameters field, but it is there for future use.
+async def get_image_by_id(image_id: str) -> ImageDB:
+ """Get the image object from the database with the provided id.
Arguments:
- app_variant -- contains the app name and variant name and optionally the parameters
- image -- contains the docker id and the tags
+ image_id (str): The image unique identifier
- Raises:
- ValueError: if variant exists or missing inputs
- """
- clean_soft_deleted_variants()
- if (
- app_variant is None
- or image is None
- or app_variant.app_name in [None, ""]
- or app_variant.variant_name in [None, ""]
- or image.docker_id in [None, ""]
- or image.tags in [None, ""]
- ):
- raise ValueError("App variant or image is None")
- if app_variant.parameters is not None:
- raise ValueError("Parameters are not supported when adding based on image")
- already_exists = any(
- [
- av
- for av in list_app_variants(show_soft_deleted=True)
- if av.app_name == app_variant.app_name
- and av.variant_name == app_variant.variant_name
- ]
+ Returns:
+ ImageDB: instance of image object
+ """
+
+ image = await engine.find_one(ImageDB, ImageDB.id == ObjectId(image_id))
+ return image
+
+
+async def fetch_app_by_id(app_id: str, **kwargs: dict) -> AppDB:
+ """Fetches an app by its ID.
+
+ Args:
+ app_id: _description_
+ """
+ assert app_id is not None, "app_id cannot be None"
+ app = await engine.find_one(AppDB, AppDB.id == ObjectId(app_id))
+ return app
+
+
+async def fetch_app_by_name(
+ app_name: str, organization_id: Optional[str] = None, **user_org_data: dict
+) -> Optional[AppDB]:
+ """Fetches an app by its name.
+
+ Args:
+ app_name (str): The name of the app to fetch.
+
+ Returns:
+ AppDB: the instance of the app
+ """
+ if not organization_id:
+ user = await get_user(user_uid=user_org_data["uid"])
+ query_expression = (AppDB.app_name == app_name) & (AppDB.user == user.id)
+ app = await engine.find_one(AppDB, query_expression)
+ else:
+ query_expression = (AppDB.app_name == app_name) & (
+ AppDB.organization == ObjectId(organization_id)
+ )
+ app = await engine.find_one(AppDB, query_expression)
+ return app
+
+
+async def fetch_app_variant_by_id(
+ app_variant_id: str,
+) -> Optional[AppVariantDB]:
+ """
+ Fetches an app variant by its ID.
+
+ Args:
+ app_variant_id (str): The ID of the app variant to fetch.
+
+ Returns:
+ AppVariantDB: The fetched app variant, or None if no app variant was found.
+ """
+ assert app_variant_id is not None, "app_variant_id cannot be None"
+ app_variant = await engine.find_one(
+ AppVariantDB, AppVariantDB.id == ObjectId(app_variant_id)
)
- if already_exists:
- raise ValueError("App variant with the same name already exists")
- with Session(engine) as session:
- # Add image
- db_image = ImageDB(**image.dict())
- session.add(db_image)
- session.commit()
- session.refresh(db_image)
- # Add app variant and link it to the app variant
- db_app_variant = AppVariantDB(image_id=db_image.id, **app_variant.dict())
- session.add(db_app_variant)
- session.commit()
- session.refresh(db_app_variant)
-
-
-def add_variant_based_on_previous(
- previous_app_variant: AppVariant,
- new_variant_name: str,
- parameters: Dict[str, Any],
-):
- """Adds a new variant from a previous/template one by changing the parameters.
+ return app_variant
- Arguments:
- app_variant -- contains the name of the app and variant
- Keyword Arguments:
- parameters -- the new parameters.
+async def fetch_base_by_id(
+ base_id: str,
+ user_org_data: dict,
+) -> Optional[VariantBaseDB]:
+ """
+ Fetches a base by its ID.
+ Args:
+ base_id (str): The ID of the base to fetch.
+ Returns:
+ VariantBaseDB: The fetched base, or None if no base was found.
+ """
+ if base_id is None:
+ raise Exception("No base_id provided")
+ base = await engine.find_one(VariantBaseDB, VariantBaseDB.id == ObjectId(base_id))
+ if base is None:
+ logger.error("Base not found")
+ return False
+ organization_id = base.organization.id
+ access = await check_user_org_access(
+ user_org_data, str(organization_id), check_owner=False
+ )
+ if not access:
+ logger.error("User does not have access to this base")
+ return False
+ return base
+
- Raises:
- ValueError: _description_
- """
- clean_soft_deleted_variants()
- if (
- previous_app_variant is None
- or previous_app_variant.app_name in [None, ""]
- or previous_app_variant.variant_name in [None, ""]
- ):
- raise ValueError("App variant is None")
- if parameters is None:
- raise ValueError("Parameters is None")
-
- # get the template variant to base the new one on
- with Session(engine) as session:
- template_variant: AppVariantDB = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.app_name == previous_app_variant.app_name)
- & (AppVariantDB.variant_name == previous_app_variant.variant_name)
+async def fetch_app_variant_by_name_and_appid(
+ variant_name: str, app_id: str
+) -> AppVariantDB:
+ """Fetch an app variant by it's name and app id.
+
+ Args:
+ variant_name (str): The name of the variant
+ app_id (str): The ID of the variant app
+
+ Returns:
+ AppVariantDB: the instance of the app variant
+ """
+
+ query_expression = (AppVariantDB.variant_name == variant_name) & (
+ AppVariantDB.app == ObjectId(app_id)
+ )
+ app_variant_db = await engine.find_one(AppVariantDB, query_expression)
+ return app_variant_db
+
+
+async def create_new_variant_base(
+ app: AppDB,
+ organization: OrganizationDB,
+ user: UserDB,
+ base_name: str,
+ image: ImageDB,
+) -> VariantBaseDB:
+ """Create a new base.
+ Args:
+ base_name (str): The name of the base.
+ image (ImageDB): The image of the base.
+ Returns:
+ VariantBaseDB: The created base.
+ """
+ logger.debug(f"Creating new base: {base_name} with image: {image} for app: {app}")
+ base = VariantBaseDB(
+ app=app,
+ organization=organization,
+ user=user,
+ base_name=base_name,
+ image=image,
+ )
+ await engine.save(base)
+ return base
+
+
+async def create_new_config(
+ config_name: str,
+ parameters: Dict,
+) -> ConfigDB:
+ """Create a new config.
+ Args:
+ config_name (str): The name of the config.
+ parameters (Dict): The parameters of the config.
+ Returns:
+ ConfigDB: The created config.
+ """
+ config_db = ConfigDB(
+ config_name=config_name,
+ parameters=parameters,
+ current_version=1,
+ version_history=[
+ ConfigVersionDB(
+ version=1, parameters=parameters, created_at=datetime.utcnow()
)
- .first()
- )
+ ],
+ )
+ await engine.save(config_db)
+ return config_db
+
+
+async def create_new_app_variant(
+ app: AppDB,
+ organization: OrganizationDB,
+ user: UserDB,
+ variant_name: str,
+ image: ImageDB,
+ base: VariantBaseDB,
+ config: ConfigDB,
+ base_name: str,
+ config_name: str,
+ parameters: Dict,
+) -> AppVariantDB:
+ """Create a new variant.
+ Args:
+ variant_name (str): The name of the variant.
+ image (ImageDB): The image of the variant.
+ base (VariantBaseDB): The base of the variant.
+ config (ConfigDB): The config of the variant.
+ Returns:
+ AppVariantDB: The created variant.
+ """
+ variant = AppVariantDB(
+ app=app,
+ organization=organization,
+ user=user,
+ variant_name=variant_name,
+ image=image,
+ base=base,
+ config=config,
+ base_name=base_name,
+ config_name=config_name,
+ parameters=parameters,
+ )
+ await engine.save(variant)
+ return variant
+
+
+async def create_image(
+ image_type: str,
+ user: UserDB,
+ deletable: bool,
+ organization: OrganizationDB,
+ template_uri: str = None,
+ docker_id: str = None,
+ tags: str = None,
+) -> ImageDB:
+ """Create a new image.
+ Args:
+ docker_id (str): The ID of the image.
+ tags (str): The tags of the image.
+ user (UserDB): The user that the image belongs to.
+ deletable (bool): Whether the image can be deleted.
+ organization (OrganizationDB): The organization that the image belongs to.
+ Returns:
+ ImageDB: The created image.
+ """
- if template_variant is None:
- print_all()
- raise ValueError("Template app variant not found")
- elif template_variant.previous_variant_name is not None:
- raise ValueError(
- "Template app variant is not a template, it is a forked variant itself"
+ # Validate image type
+ valid_image_types = ["image", "zip"]
+ if image_type not in valid_image_types:
+ raise Exception("Invalid image type")
+
+ # Validate either docker_id or template_uri, but not both
+ if (docker_id is None) == (template_uri is None):
+ raise Exception("Provide either docker_id or template_uri, but not both")
+
+ # Validate docker_id or template_uri based on image_type
+ if image_type == "image" and docker_id is None:
+ raise Exception("Docker id must be provided for type image")
+ elif image_type == "zip" and template_uri is None:
+ raise Exception("template_uri must be provided for type zip")
+
+ if image_type == "zip":
+ image = ImageDB(
+ type="zip",
+ template_uri=template_uri,
+ deletable=deletable,
+ user=user,
+ organization=organization,
+ )
+ elif image_type == "image":
+ image = ImageDB(
+ type="image",
+ docker_id=docker_id,
+ tags=tags,
+ deletable=deletable,
+ user=user,
+ organization=organization,
)
+ await engine.save(image)
+ return image
+
+
+async def create_deployment(
+ app: AppVariantDB,
+ organization: OrganizationDB,
+ user: UserDB,
+ container_name: str,
+ container_id: str,
+ uri: str,
+ status: str,
+) -> DeploymentDB:
+ """Create a new deployment.
+ Args:
+ app (AppVariantDB): The app variant to create the deployment for.
+ organization (OrganizationDB): The organization that the deployment belongs to.
+ user (UserDB): The user that the deployment belongs to.
+ container_name (str): The name of the container.
+ container_id (str): The ID of the container.
+ uri (str): The URI of the container.
+ status (str): The status of the container.
+ Returns:
+ DeploymentDB: The created deployment.
+ """
+ deployment = DeploymentDB(
+ app=app,
+ organization=organization,
+ user=user,
+ container_name=container_name,
+ container_id=container_id,
+ uri=uri,
+ status=status,
+ )
+ await engine.save(deployment)
+ return deployment
- already_exists = any(
- [
- av
- for av in list_app_variants(show_soft_deleted=True)
- if av.app_name == previous_app_variant.app_name
- and av.variant_name == new_variant_name
- ]
+
+async def create_app_and_envs(
+ app_name: str, organization_id: str, **user_org_data
+) -> AppDB:
+ """
+ Create a new app with the given name and organization ID.
+
+ Args:
+ app_name (str): The name of the app to create.
+ organization_id (str): The ID of the organization that the app belongs to.
+ **user_org_data: Additional keyword arguments.
+
+ Returns:
+ AppDB: The created app.
+
+ Raises:
+ ValueError: If an app with the same name already exists.
+ """
+
+ user_instance = await get_user(user_uid=user_org_data["uid"])
+ app = await fetch_app_by_name(app_name, organization_id, **user_org_data)
+ if app is not None:
+ raise ValueError("App with the same name already exists")
+
+ organization_db = await get_organization_object(organization_id)
+ app = AppDB(
+ app_name=app_name,
+ organization=organization_db,
+ user=user_instance,
)
- if already_exists:
- raise ValueError("App variant with the same name already exists")
+ await engine.save(app)
+ await initialize_environments(app, **user_org_data)
+ return app
- with Session(engine) as session:
- db_app_variant = AppVariantDB(
- app_name=template_variant.app_name,
- variant_name=new_variant_name,
- image_id=template_variant.image_id,
- parameters=parameters,
- previous_variant_name=template_variant.variant_name,
- )
- session.add(db_app_variant)
- session.commit()
- session.refresh(db_app_variant)
+
+async def create_user_organization(user_uid: str) -> OrganizationDB:
+ """Create a default organization for a user.
+
+ Args:
+ user_uid (str): The uid of the user
+
+ Returns:
+ OrganizationDB: Instance of OrganizationDB
+ """
+
+ user = await engine.find_one(UserDB, UserDB.uid == user_uid)
+ org_db = OrganizationDB(owner=str(user.id), type="default")
+ await engine.save(org_db)
+ return org_db
+
+
+async def get_deployment_by_objectid(
+ deployment_id: ObjectId,
+) -> DeploymentDB:
+ """Get the deployment object from the database with the provided id.
+
+ Arguments:
+ deployment_id (ObjectId): The deployment id
+
+ Returns:
+ DeploymentDB: instance of deployment object
+ """
+
+ deployment = await engine.find_one(DeploymentDB, DeploymentDB.id == deployment_id)
+ logger.debug(f"deployment: {deployment}")
+ return deployment
+
+
+async def get_organization_object(organization_id: str) -> OrganizationDB:
+ """
+ Fetches an organization by its ID.
+
+ Args:
+ organization_id (str): The ID of the organization to fetch.
+
+ Returns:
+ OrganizationDB: The fetched organization.
+ """
+ organization = await engine.find_one(
+ OrganizationDB, OrganizationDB.id == ObjectId(organization_id)
+ )
+ return organization
+
+
+async def get_organizations_by_list_ids(organization_ids: List) -> List:
+ """
+ Retrieve organizations from the database by their IDs.
+
+ Args:
+ organization_ids (List): A list of organization IDs to retrieve.
+
+ Returns:
+ List: A list of dictionaries representing the retrieved organizations.
+ """
+
+ organizations_db: List[OrganizationDB] = await engine.find(
+ OrganizationDB, OrganizationDB.id.in_(organization_ids)
+ )
+
+ return organizations_db
-def list_app_variants(
- app_name: str = None, show_soft_deleted=False
-) -> List[AppVariant]:
+async def list_app_variants_for_app_id(
+ app_id: str, **kwargs: dict
+) -> List[AppVariantDB]:
"""
Lists all the app variants from the db
Args:
app_name: if specified, only returns the variants for the app name
- show_soft_deleted: if true, returns soft deleted variants as well
Returns:
List[AppVariant]: List of AppVariant objects
"""
- clean_soft_deleted_variants()
- with Session(engine) as session:
- query = session.query(AppVariantDB)
- if not show_soft_deleted:
- query = query.filter(AppVariantDB.is_deleted == False)
- if app_name is not None:
- query = query.filter(AppVariantDB.app_name == app_name)
-
- subquery = (
- session.query(AppVariantDB.app_name, AppVariantDB.variant_name)
- .group_by(AppVariantDB.app_name, AppVariantDB.variant_name)
- .subquery()
- )
+ assert app_id is not None, "app_id cannot be None"
+ query_expression = AppVariantDB.app == ObjectId(app_id)
+ app_variants_db: List[AppVariantDB] = await engine.find(
+ AppVariantDB,
+ query_expression,
+ sort=(AppVariantDB.variant_name),
+ )
+
+ return app_variants_db
- query = query.join(
- subquery,
- and_(
- AppVariantDB.app_name == subquery.c.app_name,
- AppVariantDB.variant_name == subquery.c.variant_name,
- ),
- )
- app_variants_db: List[AppVariantDB] = query.all()
- # Include previous variant name
- app_variants: List[AppVariant] = []
- for av in app_variants_db:
- app_variant = app_variant_db_to_pydantic(av)
- app_variants.append(app_variant)
- return app_variants
+async def list_bases_for_app_id(
+ app_id: str, base_name: Optional[str] = None, **kwargs: dict
+) -> List[VariantBaseDB]:
+ assert app_id is not None, "app_id cannot be None"
+ query_expression = VariantBaseDB.app == ObjectId(app_id)
+ if base_name:
+ query_expression = query_expression & query.eq(
+ VariantBaseDB.base_name, base_name
+ )
+ bases_db: List[VariantBaseDB] = await engine.find(
+ VariantBaseDB,
+ query_expression,
+ sort=(VariantBaseDB.base_name),
+ )
+ return bases_db
-def list_apps() -> List[App]:
+async def list_variants_for_base(
+ base: VariantBaseDB, **kwargs: dict
+) -> List[AppVariantDB]:
"""
- Lists all the unique app names from the database
+ Lists all the app variants from the db for a base
+ Args:
+ base: if specified, only returns the variants for the base
+ Returns:
+ List[AppVariant]: List of AppVariant objects
"""
- clean_soft_deleted_variants()
- with Session(engine) as session:
- app_names = session.query(AppVariantDB.app_name).distinct().all()
- # Unpack tuples to create a list of strings instead of a list of tuples
- return [App(app_name=name) for (name,) in app_names]
+ assert base is not None, "base cannot be None"
+ query_expression = AppVariantDB.base == ObjectId(base.id)
+ app_variants_db: List[AppVariantDB] = await engine.find(
+ AppVariantDB,
+ query_expression,
+ sort=(AppVariantDB.variant_name),
+ )
+ return app_variants_db
-def get_image(app_variant: AppVariant) -> Image:
- """Returns the image associated with the app variant
+
+async def get_user(user_uid: str) -> UserDB:
+ """Get the user object from the database.
Arguments:
- app_variant -- AppVariant to fetch the image for
+ user_id (str): The user unique identifier
Returns:
- Image -- The Image associated with the app variant
+ UserDB: instance of user
"""
- with Session(engine) as session:
- db_app_variant: AppVariantDB = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.app_name == app_variant.app_name)
- & (AppVariantDB.variant_name == app_variant.variant_name)
- )
- .first()
- )
- if db_app_variant:
- image_db: ImageDB = (
- session.query(ImageDB)
- .filter(ImageDB.id == db_app_variant.image_id)
- .first()
- )
- return image_db_to_pydantic(image_db)
+ user = await engine.find_one(UserDB, UserDB.uid == user_uid)
+ if user is None:
+ if os.environ["FEATURE_FLAG"] not in ["cloud", "ee"]:
+ create_user = UserDB(uid="0")
+ await engine.save(create_user)
+
+ org = OrganizationDB(type="default", owner=str(create_user.id))
+ await engine.save(org)
+
+ create_user.organizations.append(org.id)
+ await engine.save(create_user)
+ await engine.save(org)
+
+ return create_user
else:
- raise Exception("App variant not found")
+ raise Exception("Please login or signup")
+ else:
+ return user
-def remove_app_variant(app_variant: AppVariant):
- """Remove an app variant from the db
- the logic for removing the image is in app_manager.py
+async def get_user_with_id(user_id: ObjectId):
+ """
+ Retrieves a user from a database based on their ID.
- Arguments:
- app_variant -- AppVariant to remove
+ Args:
+ user_id (ObjectId): The ID of the user to retrieve from the database.
+
+ Returns:
+ user: The user object retrieved from the database.
+
+ Raises:
+ Exception: If an error occurs while getting the user from the database.
"""
- if (
- app_variant is None
- or app_variant.app_name in [None, ""]
- or app_variant.variant_name in [None, ""]
- ):
- raise ValueError("App variant is None")
- with Session(engine) as session:
- app_variant_db = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.app_name == app_variant.app_name)
- & (AppVariantDB.variant_name == app_variant.variant_name)
- )
- .first()
- )
- if app_variant_db is None:
- raise ValueError("App variant not found")
-
- if app_variant_db.previous_variant_name is not None: # forked variant
- session.delete(app_variant_db)
- elif check_is_last_variant(
- app_variant_db
- ): # last variant using the image, okay to delete
- session.delete(app_variant_db)
- else:
- app_variant_db.is_deleted = True # soft deletion
- session.commit()
+ try:
+ user = await engine.find_one(UserDB, UserDB.id == user_id)
+ return user
+ except Exception as e:
+ logger.error(f"Failed to get user with id: {e}")
+ raise Exception(f"Error while getting user: {e}")
-def remove_image(image: Image):
- """Remove image from db based on pydantic class
+async def get_user_with_email(email: str):
+ """
+ Retrieves a user from the database based on their email address.
- Arguments:
- image -- Image to remove
+ Args:
+ email (str): The email address of the user to retrieve.
+
+ Returns:
+ UserDB: The user object retrieved from the database.
+
+ Raises:
+ Exception: If a valid email address is not provided.
+ Exception: If an error occurs while retrieving the user.
+
+ Example Usage:
+ user = await get_user_with_email('example@example.com')
"""
- if image is None or image.docker_id in [None, ""] or image.tags in [None, ""]:
- raise ValueError("Image is None")
- with Session(engine) as session:
- image_db = (
- session.query(ImageDB)
- .filter(
- (ImageDB.docker_id == image.docker_id) & (ImageDB.tags == image.tags)
- )
- .first()
- )
- if image_db is None:
- raise ValueError("Image not found")
- session.delete(image_db)
- session.commit()
+ if "@" not in email:
+ raise Exception("Please provide a valid email address")
+ try:
+ user = await engine.find_one(UserDB, UserDB.email == email)
+ return user
+ except Exception as e:
+ logger.error(f"Failed to get user with email address: {e}")
+ raise Exception(f"Error while getting user: {e}")
-def check_is_last_variant(db_app_variant: AppVariantDB) -> bool:
- """Checks whether the input variant is the sole variant that uses its linked image
- This is a helpful function to determine whether to delete the image when removing a variant
- Usually many variants will use the same image (these variants would have been created using the UI)
- We only delete the image and shutdown the container if the variant is the last one using the image
- Arguments:
- app_variant -- AppVariant to check
+async def get_users_by_ids(user_ids: List) -> List:
+ """
+ Retrieve users from the database by their IDs.
+
+ Args:
+ user_ids (List): A list of user IDs to retrieve.
+
Returns:
- true if it's the last variant, false otherwise
+ List: A list of dictionaries representing the retrieved users.
"""
- with Session(engine) as session:
- # If it's the only variant left that uses the image, delete the image
- if (
- session.query(AppVariantDB)
- .filter(AppVariantDB.image_id == db_app_variant.image_id)
- .count()
- == 1
- ):
- return True
- else:
- return False
+
+ users_db: List[UserDB] = await engine.find(UserDB, UserDB.id.in_(user_ids))
+
+ return users_db
-def get_variant_from_db(app_variant: AppVariant) -> AppVariantDB:
- """Checks whether the app variant exists in our db
- and returns the AppVariantDB object if it does
+async def get_orga_image_instance_by_docker_id(
+ organization_id: str, docker_id: str
+) -> ImageDB:
+ """Get the image object from the database with the provided id.
Arguments:
- app_variant -- AppVariant to check
+ organization_id (str): The orga unique identifier
+ docker_id (str): The image id
Returns:
- AppVariantDB -- The AppVariantDB object if it exists, None otherwise
+ ImageDB: instance of image object
"""
- with Session(engine) as session:
- # Find app_variant in the database
- db_app_variant: AppVariantDB = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.app_name == app_variant.app_name)
- & (AppVariantDB.variant_name == app_variant.variant_name)
- )
- .first()
- )
- logger.info(f"Found app variant: {db_app_variant}")
- if db_app_variant:
- return db_app_variant
- else:
- return None
+ query_expression = (ImageDB.organization == ObjectId(organization_id)) & query.eq(
+ ImageDB.docker_id, docker_id
+ )
+ image = await engine.find_one(ImageDB, query_expression)
+ return image
-def print_all():
- """Prints all the tables in the database"""
- with Session(engine) as session:
- for app_variant in session.query(AppVariantDB).all():
- helpers.print_app_variant(app_variant)
- for image in session.query(ImageDB).all():
- helpers.print_image(image)
+async def get_orga_image_instance_by_uri(
+ organization_id: str, template_uri: str
+) -> ImageDB:
+ """Get the image object from the database with the provided id.
-def clean_soft_deleted_variants():
- """Remove soft-deleted app variants if their image is not used by any existing variant."""
- with Session(engine) as session:
- # Get all soft-deleted app variants
- soft_deleted_variants: List[AppVariantDB] = (
- session.query(AppVariantDB).filter(AppVariantDB.is_deleted == True).all()
- )
+ Arguments:
+ organization_id (str): The orga unique identifier
+ template_uri (url): The image template url
- for variant in soft_deleted_variants:
- # Get non-deleted variants that use the same image
- image_used = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.image_id == variant.image_id)
- & (AppVariantDB.is_deleted == False)
- )
- .first()
- )
+ Returns:
+ ImageDB: instance of image object
+ """
+ parsed_url = urlparse(template_uri)
- # If the image is not used by any non-deleted variant, delete the variant
- if image_used is None:
- session.delete(variant)
+ if not parsed_url.scheme and not parsed_url.netloc:
+ raise ValueError(f"Invalid URL: {template_uri}")
- session.commit()
+ query_expression = (ImageDB.organization == ObjectId(organization_id)) & query.eq(
+ ImageDB.template_uri, template_uri
+ )
+ image = await engine.find_one(ImageDB, query_expression)
+ return image
-def update_variant_parameters(app_variant: AppVariant, parameters: Dict[str, Any]):
- """Updates the parameters of a specific variant
+async def get_app_instance_by_id(app_id: str) -> AppDB:
+ """Get the app object from the database with the provided id.
Arguments:
- app_variant -- contains the name of the app and variant
- parameters -- the new parameters.
+ app_id (str): The app unique identifier
- Raises:
- ValueError: If the variant doesn't exist or parameters is None.
- """
- if (
- app_variant is None
- or app_variant.app_name in [None, ""]
- or app_variant.variant_name in [None, ""]
- ):
- raise ValueError("App variant is None")
- if parameters is None:
- raise ValueError("Parameters is None")
-
- with Session(engine) as session:
- db_app_variant: AppVariantDB = (
- session.query(AppVariantDB)
- .filter(
- (AppVariantDB.app_name == app_variant.app_name)
- & (AppVariantDB.variant_name == app_variant.variant_name)
+ Returns:
+ AppDB: instance of app object
+ """
+
+ app = await engine.find_one(AppDB, AppDB.id == ObjectId(app_id))
+ return app
+
+
+async def add_variant_from_base_and_config(
+ base_db: VariantBaseDB,
+ new_config_name: str,
+ parameters: Dict[str, Any],
+ **user_org_data: dict,
+):
+ """
+ Add a new variant to the database based on an existing base and a new configuration.
+
+ Args:
+ base_db (VariantBaseDB): The existing base to use as a template for the new variant.
+ new_config_name (str): The name of the new configuration to use for the new variant.
+ parameters (Dict[str, Any]): The parameters to use for the new configuration.
+ **user_org_data (dict): Additional user and organization data.
+
+ Returns:
+ AppVariantDB: The newly created app variant.
+ """
+ new_variant_name = f"{base_db.base_name}.{new_config_name}"
+ previous_app_variant_db = await find_previous_variant_from_base_id(str(base_db.id))
+ if previous_app_variant_db is None:
+ logger.error("Failed to find the previous app variant in the database.")
+ raise HTTPException(status_code=500, detail="Previous app variant not found")
+ logger.debug(f"Located previous variant: {previous_app_variant_db}")
+ app_variant_for_base = await list_variants_for_base(base_db)
+
+ already_exists = any(
+ [av for av in app_variant_for_base if av.config_name == new_config_name]
+ )
+ if already_exists:
+ raise ValueError("App variant with the same name already exists")
+ user_db = await get_user(user_uid=user_org_data["uid"])
+ config_db = ConfigDB(
+ config_name=new_config_name,
+ parameters=parameters,
+ current_version=1,
+ version_history=[
+ ConfigVersionDB(
+ version=1, parameters=parameters, created_at=datetime.utcnow()
)
- .first()
- )
+ ],
+ )
+ await engine.save(config_db)
+ db_app_variant = AppVariantDB(
+ app=previous_app_variant_db.app,
+ variant_name=new_variant_name,
+ image=base_db.image,
+ user=user_db,
+ organization=previous_app_variant_db.organization,
+ parameters=parameters,
+ previous_variant_name=previous_app_variant_db.variant_name, # TODO: Remove in future
+ base_name=base_db.base_name,
+ base=base_db,
+ config_name=new_config_name,
+ config=config_db,
+ is_deleted=False,
+ )
+ await engine.save(db_app_variant)
+ return db_app_variant
+
+
+async def list_apps(
+ app_name: str = None, org_id: str = None, **user_org_data: dict
+) -> List[App]:
+ """
+ Lists all the unique app names and their IDs from the database
+
+ Errors:
+ JSONResponse: You do not have permission to access this organization; status_code: 403
+
+ Returns:
+ List[App]
+ """
- if db_app_variant is None:
- raise ValueError("App variant not found")
+ user = await get_user(user_uid=user_org_data["uid"])
+ assert user is not None, "User is None"
+
+ if app_name is not None:
+ app_db = await fetch_app_by_name(app_name, org_id, **user_org_data)
+ return [app_db_to_pydantic(app_db)]
+ elif org_id is not None:
+ organization_access = await check_user_org_access(user_org_data, org_id)
+ if organization_access:
+ apps: List[AppDB] = await engine.find(
+ AppDB, AppDB.organization == ObjectId(org_id)
+ )
+ return [app_db_to_pydantic(app) for app in apps]
- # Update parameters
- if db_app_variant.parameters is not None and set(
- db_app_variant.parameters.keys()
- ) != set(parameters.keys()):
- logger.error(
- f"Parameters keys don't match: {db_app_variant.parameters.keys()} vs {parameters.keys()}"
+ else:
+ return JSONResponse(
+ {"error": "You do not have permission to access this organization"},
+ status_code=403,
)
- raise ValueError("Parameters keys don't match")
- db_app_variant.parameters = parameters
- session.commit()
+ else:
+ apps: List[AppVariantDB] = await engine.find(AppDB, AppDB.user == user.id)
+ return [app_db_to_pydantic(app) for app in apps]
-def remove_old_template_from_db(template_ids: list) -> None:
- """Deletes old templates that are no longer in docker hub.
- Arguments:
- template_ids -- list of template IDs you want to keep
+async def list_app_variants(app_id: str = None, **kwargs: dict) -> List[AppVariantDB]:
+ """
+ Lists all the app variants from the db
+ Args:
+ app_name: if specified, only returns the variants for the app name
+ Returns:
+ List[AppVariant]: List of AppVariant objects
"""
- with Session(engine) as session:
- temps_to_delete = (
- session.query(TemplateDB)
- .filter(~TemplateDB.template_id.in_(template_ids))
- .all()
- )
- for template in temps_to_delete:
- session.delete(template)
- session.commit()
+ # Construct query expressions
+ logger.debug("app_id: %s", app_id)
+ query_filters = query.QueryExpression()
+ if app_id is not None:
+ query_filters = query_filters & (AppVariantDB.app == ObjectId(app_id))
+ logger.debug("query_filters: %s", query_filters)
+ app_variants_db: List[AppVariantDB] = await engine.find(AppVariantDB, query_filters)
+
+ # Include previous variant name
+ return app_variants_db
+
+
+async def check_is_last_variant_for_image(
+ db_app_variant: AppVariantDB,
+) -> bool:
+ """Checks whether the input variant is the sole variant that uses its linked image
+ This is a helpful function to determine whether to delete the image when removing a variant
+ Usually many variants will use the same image (these variants would have been created using the UI)
+ We only delete the image and shutdown the container if the variant is the last one using the image
+
+ Arguments:
+ app_variant -- AppVariant to check
+ Returns:
+ true if it's the last variant, false otherwise
+ """
+
+ # Build the query expression for the two conditions
+ logger.debug("db_app_variant: %s", db_app_variant)
+ query_expression = (
+ AppVariantDB.organization == ObjectId(db_app_variant.organization.id)
+ ) & (AppVariantDB.base == ObjectId(db_app_variant.base.id))
+ # Count the number of variants that match the query expression
+ count_variants = await engine.count(AppVariantDB, query_expression)
+
+ # If it's the only variant left that uses the image, delete the image
+ return bool(count_variants == 1)
+
+
+async def remove_deployment(deployment_db: DeploymentDB, **kwargs: dict):
+ """Remove a deployment from the db
+
+ Arguments:
+ deployment -- Deployment to remove
+ """
+ logger.debug("Removing deployment")
+ assert deployment_db is not None, "deployment_db is missing"
+
+ await engine.delete(deployment_db)
+
+
+async def remove_app_variant_from_db(app_variant_db: AppVariantDB, **kwargs: dict):
+ """Remove an app variant from the db
+ the logic for removing the image is in app_manager.py
+
+ Arguments:
+ app_variant -- AppVariant to remove
+ """
+ logger.debug("Removing app variant")
+ assert app_variant_db is not None, "app_variant_db is missing"
+
+ # Remove the variant from the associated environments
+ logger.debug("list_environments_by_variant")
+ environments = await list_environments_by_variant(
+ app_variant_db,
+ **kwargs,
+ )
+ for environment in environments:
+ environment.deployed_app_variant = None
+ await engine.save(environment)
+ # removing the config
+ config = app_variant_db.config
+ await engine.delete(config)
+
+ await engine.delete(app_variant_db)
+
+
+async def deploy_to_environment(environment_name: str, variant_id: str, **kwargs: dict):
+ """
+ Deploys an app variant to a specified environment.
+
+ Args:
+ environment_name (str): The name of the environment to deploy the app variant to.
+ variant_id (str): The ID of the app variant to deploy.
+ **kwargs (dict): Additional keyword arguments.
+
+ Raises:
+ ValueError: If the app variant is not found or if the environment is not found or if the app variant is already
+ deployed to the environment.
+
+ Returns:
+ None
+ """
+ app_variant_db = await fetch_app_variant_by_id(variant_id)
+ if app_variant_db is None:
+ raise ValueError("App variant not found")
+
+ # Find the environment for the given app name and user
+ query_filters = (
+ AppEnvironmentDB.app == ObjectId(app_variant_db.app.id)
+ ) & query.eq(AppEnvironmentDB.name, environment_name)
+ environment_db: AppEnvironmentDB = await engine.find_one(
+ AppEnvironmentDB, query_filters
+ )
+ if environment_db is None:
+ raise ValueError(f"Environment {environment_name} not found")
+ if environment_db.deployed_app_variant == app_variant_db:
+ raise ValueError(
+ f"Variant {app_variant_db.app.app_name}/{app_variant_db.variant_name} is already deployed to the environment {environment_name}"
+ )
+
+ # Update the environment with the new variant name
+ environment_db.deployed_app_variant = app_variant_db.id
+ await engine.save(environment_db)
+
+
+async def list_environments(app_id: str, **kwargs: dict) -> List[AppEnvironmentDB]:
+ """
+ List all environments for a given app ID.
+
+ Args:
+ app_id (str): The ID of the app to list environments for.
+ **kwargs (dict): Additional keyword arguments.
+
+ Returns:
+ List[AppEnvironmentDB]: A list of AppEnvironmentDB objects representing the environments for the given app ID.
+ """
+ logging.debug("Listing environments for app %s", app_id)
+ app_instance = await fetch_app_by_id(app_id=app_id)
+ if app_instance is None:
+ logging.error(f"App with id {app_id} not found")
+ raise ValueError("App not found")
+
+ environments_db: List[AppEnvironmentDB] = await engine.find(
+ AppEnvironmentDB, AppEnvironmentDB.app == ObjectId(app_id)
+ )
+
+ return environments_db
+
+
+async def initialize_environments(
+ app_db: AppDB, **kwargs: dict
+) -> List[AppEnvironmentDB]:
+ """
+ Initializes the environments for the app with the given database.
+
+ Args:
+ app_db (AppDB): The database for the app.
+ **kwargs (dict): Additional keyword arguments.
+
+ Returns:
+ List[AppEnvironmentDB]: A list of the initialized environments.
+ """
+ environments = []
+ for env_name in ["development", "staging", "production"]:
+ env = await create_environment(name=env_name, app_db=app_db, **kwargs)
+ environments.append(env)
+ return environments
+
+
+async def create_environment(
+ name: str, app_db: AppDB, **kwargs: dict
+) -> AppEnvironmentDB:
+ """
+ Creates a new environment in the database.
+
+ Args:
+ name (str): The name of the environment.
+ app_db (AppDB): The AppDB object representing the app that the environment belongs to.
+ **kwargs (dict): Additional keyword arguments.
+
+ Returns:
+ AppEnvironmentDB: The newly created AppEnvironmentDB object.
+ """
+ environment_db = AppEnvironmentDB(
+ app=app_db,
+ name=name,
+ user=app_db.user,
+ organization=app_db.organization,
+ )
+ await engine.save(environment_db)
+ return environment_db
+
+
+async def list_environments_by_variant(
+ app_variant: AppVariantDB, **kwargs: dict
+) -> List[AppEnvironmentDB]:
+ """
+ Returns a list of environments for a given app variant.
+
+ Args:
+ app_variant (AppVariantDB): The app variant to retrieve environments for.
+ **kwargs (dict): Additional keyword arguments.
+
+ Returns:
+ List[AppEnvironmentDB]: A list of AppEnvironmentDB objects.
+ """
+
+ environments_db: List[AppEnvironmentDB] = await engine.find(
+ AppEnvironmentDB,
+ (AppEnvironmentDB.app == ObjectId(app_variant.app.id)),
+ )
+
+ return environments_db
+
+
+async def remove_image(image: ImageDB, **kwargs: dict):
+ """
+ Removes an image from the database.
+
+ Args:
+ image (ImageDB): The image to remove from the database.
+ **kwargs (dict): Additional keyword arguments.
+
+ Raises:
+ ValueError: If the image is None.
+
+ Returns:
+ None
+ """
+ if image is None:
+ raise ValueError("Image is None")
+ await engine.delete(image)
+
+
+async def remove_environment(environment_db: AppEnvironmentDB, **kwargs: dict):
+ """
+ Removes an environment from the database.
+
+ Args:
+ environment_db (AppEnvironmentDB): The environment to remove from the database.
+ **kwargs (dict): Additional keyword arguments.
+
+ Raises:
+ AssertionError: If environment_db is None.
+
+ Returns:
+ None
+ """
+ assert environment_db is not None, "environment_db is missing"
+ await engine.delete(environment_db)
+
+
+async def remove_app_testsets(app_id: str, **kwargs):
+ """Returns a list of testsets owned by an app.
+
+ Args:
+ app_id (str): The name of the app
+
+ Returns:
+ int: The number of testsets deleted
+ """
+
+ # Get user object
+ # Find testsets owned by the app
+ deleted_count: int = 0
+
+ # Build query expression
+ testsets = await engine.find(TestSetDB, TestSetDB.app == ObjectId(app_id))
+
+ # Perform deletion if there are testsets to delete
+ if testsets is not None:
+ for testset in testsets:
+ await engine.delete(testset)
+ deleted_count += 1
+ logger.info(f"{deleted_count} testset(s) deleted for app {app_id}")
+ return deleted_count
+
+ logger.info(f"No testsets found for app {app_id}")
+ return 0
+
+
+async def remove_base_from_db(base: VariantBaseDB, **kwargs):
+ """
+ Remove a base from the database.
+
+ Args:
+ base (VariantBaseDB): The base to be removed from the database.
+ **kwargs: Additional keyword arguments.
+
+ Raises:
+ ValueError: If the base is None.
+
+ Returns:
+ None
+ """
+ if base is None:
+ raise ValueError("Base is None")
+ await engine.delete(base)
+
+
+async def remove_app_by_id(app_id: str, **kwargs):
+ """
+ Removes an app instance from the database by its ID.
+
+ Args:
+ app_id (str): The ID of the app instance to remove.
+
+ Raises:
+ AssertionError: If app_id is None or if the app instance could not be found.
+
+ Returns:
+ None
+ """
+ assert app_id is not None, "app_id cannot be None"
+ app_instance = await fetch_app_by_id(app_id=app_id)
+ assert app_instance is not None, f"app instance for {app_id} could not be found"
+ await engine.delete(app_instance)
+
+
+async def update_variant_parameters(
+ app_variant_db: AppVariantDB, parameters: Dict[str, Any], **kwargs: dict
+) -> None:
+ """
+ Update the parameters of an app variant in the database.
+
+ Args:
+ app_variant_db (AppVariantDB): The app variant to update.
+ parameters (Dict[str, Any]): The new parameters to set for the app variant.
+ **kwargs (dict): Additional keyword arguments.
+
+ Raises:
+ ValueError: If there is an issue updating the variant parameters.
+ """
+ assert app_variant_db is not None, "app_variant is missing"
+ assert parameters is not None, "parameters is missing"
+
+ try:
+ logging.debug("Updating variant parameters")
+
+ # Update associated ConfigDB parameters and versioning
+ config_db = app_variant_db.config
+ new_version = config_db.current_version + 1
+ config_db.version_history.append(
+ ConfigVersionDB(
+ version=new_version,
+ parameters=config_db.parameters,
+ created_at=datetime.utcnow(),
+ )
+ )
+ config_db.current_version = new_version
+ config_db.parameters = parameters
+
+ # Save updated ConfigDB
+ await engine.save(config_db)
+
+ except Exception as e:
+ logging.error(f"Issue updating variant parameters: {e}")
+ raise ValueError("Issue updating variant parameters")
+
+
+async def get_app_variant_by_app_name_and_environment(
+ app_id: str, environment: str, **kwargs: dict
+) -> Optional[AppVariantDB]:
+ """
+ Retrieve the deployed app variant for a given app and environment.
+
+ Args:
+ app_id (str): The ID of the app to retrieve the variant for.
+ environment (str): The name of the environment to retrieve the variant for.
+ **kwargs (dict): Additional keyword arguments to pass to the function.
+
+ Returns:
+ Optional[AppVariantDB]: The deployed app variant for the given app and environment, or None if not found.
+ """
+ # Get the environment
+ # Construct query filters for finding the environment in the database
+ query_filters_for_environment = query.eq(AppEnvironmentDB.name, environment) & (
+ AppEnvironmentDB.app == ObjectId(app_id)
+ )
+
+ # Perform the database query to find the environment
+ environment_db = await engine.find_one(
+ AppEnvironmentDB, query_filters_for_environment
+ )
+
+ if not environment_db:
+ logger.info(f"Environment {environment} not found")
+ return None
+ if environment_db.deployed_app_variant is None:
+ logger.info(f"No variant deployed to environment {environment}")
+ return None
+
+ app_variant_db = await get_app_variant_instance_by_id(
+ str(environment_db.deployed_app_variant)
+ )
+
+ return app_variant_db
+
+
+async def get_app_variant_instance_by_id(variant_id: str):
+ """Get the app variant object from the database with the provided id.
+
+ Arguments:
+ variant_id (str): The app variant unique identifier
+
+ Returns:
+ AppVariantDB: instance of app variant object
+ """
+
+ app_variant_db = await engine.find_one(
+ AppVariantDB, AppVariantDB.id == ObjectId(variant_id)
+ )
+ return app_variant_db
+
+
+async def fetch_testset_by_id(testset_id: str) -> Optional[TestSetDB]:
+ """Fetches a testset by its ID.
+ Args:
+ testset_id (str): The ID of the testset to fetch.
+ Returns:
+ TestSetDB: The fetched testset, or None if no testset was found.
+ """
+ assert testset_id is not None, "testset_id cannot be None"
+ testset = await engine.find_one(TestSetDB, TestSetDB.id == ObjectId(testset_id))
+ return testset
+
+
+async def fetch_testsets_by_app_id(app_id: str) -> List[TestSetDB]:
+ """Fetches all testsets for a given app.
+ Args:
+ app_id (str): The ID of the app to fetch testsets for.
+ Returns:
+ List[TestSetDB]: The fetched testsets.
+ """
+ assert app_id is not None, "app_id cannot be None"
+ testsets = await engine.find(TestSetDB, TestSetDB.app == ObjectId(app_id))
+ return testsets
+
+
+async def fetch_evaluation_by_id(evaluation_id: str) -> Optional[EvaluationDB]:
+ """Fetches a evaluation by its ID.
+ Args:
+ evaluation_id (str): The ID of the evaluation to fetch.
+ Returns:
+ EvaluationDB: The fetched evaluation, or None if no evaluation was found.
+ """
+ assert evaluation_id is not None, "evaluation_id cannot be None"
+ evaluation = await engine.find_one(
+ EvaluationDB, EvaluationDB.id == ObjectId(evaluation_id)
+ )
+ return evaluation
+
+
+async def fetch_evaluation_scenario_by_id(
+ evaluation_scenario_id: str,
+) -> Optional[EvaluationScenarioDB]:
+ """Fetches and evaluation scenario by its ID.
+ Args:
+ evaluation_scenario_id (str): The ID of the evaluation scenario to fetch.
+ Returns:
+ EvaluationScenarioDB: The fetched evaluation scenario, or None if no evaluation scenario was found.
+ """
+ assert evaluation_scenario_id is not None, "evaluation_scenario_id cannot be None"
+ evaluation_scenario = await engine.find_one(
+ EvaluationScenarioDB,
+ EvaluationScenarioDB.id == ObjectId(evaluation_scenario_id),
+ )
+ return evaluation_scenario
+
+
+async def find_previous_variant_from_base_id(
+ base_id: str,
+) -> Optional[AppVariantDB]:
+ """Find the previous variant from a base id.
+
+ Args:
+ base_id (str): The base id to search for.
+
+ Returns:
+ Optional[AppVariantDB]: The previous variant, or None if no previous variant was found.
+ """
+ assert base_id is not None, "base_id cannot be None"
+ previous_variants = await engine.find(
+ AppVariantDB, AppVariantDB.base == ObjectId(base_id)
+ )
+ logger.debug("previous_variants: %s", previous_variants)
+ if len(list(previous_variants)) == 0:
+ return None
+ # select the variant for which previous_variant_name is None
+ for previous_variant in previous_variants:
+ # if previous_variant.previous_variant_name is None:
+ # logger.debug("previous_variant: %s", previous_variant)
+ return previous_variant # we don't care which variant do we return
+ assert False, "None of the previous variants has previous_variant_name=None"
+
+
+async def add_template(**kwargs: dict) -> str:
+ """
+ Adds a new template to the database.
+
+ Args:
+ **kwargs (dict): Keyword arguments containing the template data.
+
+ Returns:
+ template_id (Str): The Id of the created template.
+ """
+ existing_template = await engine.find_one(
+ TemplateDB, TemplateDB.tag_id == kwargs["tag_id"]
+ )
+ if existing_template is None:
+ db_template = TemplateDB(**kwargs)
+ await engine.save(db_template)
+ return str(db_template.id)
+
+
+async def add_zip_template(key, value):
+ """
+ Adds a new s3 zip template to the database
+
+ Args:
+ key: key of the json file
+ value (dict): dictionary value of a key
+
+ Returns:
+ template_id (Str): The Id of the created template.
+ """
+ existing_template = await engine.find_one(TemplateDB, TemplateDB.name == key)
+
+ if existing_template:
+ # Compare existing values with new values
+ if (
+ existing_template.title == value.get("name")
+ and existing_template.description == value.get("description")
+ and existing_template.template_uri == value.get("template_uri")
+ ):
+ # Values are unchanged, return existing template id
+ return str(existing_template.id)
+ else:
+ # Values are changed, delete existing template
+ await engine.delete(existing_template)
+
+ # Create a new template
+ template_name = key
+ title = value.get("name")
+ description = value.get("description")
+ template_uri = value.get("template_uri")
+
+ template_db_instance = TemplateDB(
+ type="zip",
+ name=template_name,
+ title=title,
+ description=description,
+ template_uri=template_uri,
+ )
+ await engine.save(template_db_instance)
+ return str(template_db_instance.id)
+
+
+async def get_template(template_id: str) -> TemplateDB:
+ """
+ Fetches a template by its ID.
+
+ Args:
+ template_id (str): The ID of the template to fetch.
+
+ Returns:
+ TemplateDB: The fetched template.
+ """
+
+ assert template_id is not None, "template_id cannot be None"
+ template_db = await engine.find_one(
+ TemplateDB, TemplateDB.id == ObjectId(template_id)
+ )
+ return template_db
+
+
+async def remove_old_template_from_db(tag_ids: list) -> None:
+ """Deletes old templates that are no longer in docker hub.
+
+ Arguments:
+ tag_ids -- list of template IDs you want to keep
+ """
+
+ templates_to_delete = []
+ try:
+ templates: List[TemplateDB] = await engine.find(TemplateDB)
+
+ for temp in templates:
+ if temp.tag_id not in tag_ids:
+ templates_to_delete.append(temp)
+
+ for template in templates_to_delete:
+ await engine.delete(template)
+ except DocumentParsingError as exc:
+ remove_document_using_driver(str(exc.primary_value), "templates")
+
+
+def remove_document_using_driver(document_id: str, collection_name: str) -> None:
+ """Deletes document from using pymongo driver"""
+
+ import pymongo
+
+ client = pymongo.MongoClient(os.environ["MONGODB_URI"])
+ db = client.get_database("agenta_v2")
+
+ collection = db.get_collection(collection_name)
+ deleted = collection.delete_one({"_id": ObjectId(document_id)})
+ print(
+ f"Deleted documents in {collection_name} collection. Acknowledged: {deleted.acknowledged}"
+ )
+
+
+async def get_templates() -> List[Template]:
+ templates = await engine.find(TemplateDB)
+ return templates_db_to_pydantic(templates)
+
+
+async def count_apps(**user_org_data: dict) -> int:
+ """
+ Counts all the unique app names from the database
+ """
+
+ # Get user object
+ user = await get_user(user_uid=user_org_data["uid"])
+ if user is None:
+ return 0
+
+ no_of_apps = await engine.count(AppVariantDB, AppVariantDB.user == user.id)
+ return no_of_apps
+
+
+async def update_base(
+ base: VariantBaseDB,
+ **kwargs: dict,
+) -> VariantBaseDB:
+ """Update the base object in the database with the provided id.
+
+ Arguments:
+ base (VariantBaseDB): The base object to update.
+ """
+ for key, value in kwargs.items():
+ if key in base.__fields__:
+ setattr(base, key, value)
+ await engine.save(base)
+ return base
+
+
+async def update_app_variant(
+ app_variant: AppVariantDB,
+ **kwargs: dict,
+) -> AppVariantDB:
+ """Update the app variant object in the database with the provided id.
+
+ Arguments:
+ app_variant (AppVariantDB): The app variant object to update.
+ """
+ for key, value in kwargs.items():
+ if key in app_variant.__fields__:
+ setattr(app_variant, key, value)
+ await engine.save(app_variant)
+ return app_variant
+
+
+async def fetch_base_and_check_access(
+ base_id: str, user_org_data: dict, check_owner=False
+):
+ """
+ Fetches a base from the database and checks if the user has access to it.
+
+ Args:
+ base_id (str): The ID of the base to fetch.
+ user_org_data (dict): The user's organization data.
+ check_owner (bool, optional): Whether to check if the user is the owner of the base. Defaults to False.
+
+ Raises:
+ Exception: If no base_id is provided.
+ HTTPException: If the base is not found or the user does not have access to it.
+
+ Returns:
+ VariantBaseDB: The fetched base.
+ """
+ if base_id is None:
+ raise Exception("No base_id provided")
+ base = await engine.find_one(VariantBaseDB, VariantBaseDB.id == ObjectId(base_id))
+ if base is None:
+ logger.error("Base not found")
+ raise HTTPException(status_code=404, detail="Base not found")
+ organization_id = base.organization.id
+ access = await check_user_org_access(
+ user_org_data, str(organization_id), check_owner
+ )
+ if not access:
+ error_msg = f"You do not have access to this base: {base_id}"
+ raise HTTPException(status_code=403, detail=error_msg)
+ return base
+
+
+async def fetch_app_and_check_access(
+ app_id: str, user_org_data: dict, check_owner=False
+):
+ """
+ Fetches an app from the database and checks if the user has access to it.
+
+ Args:
+ app_id (str): The ID of the app to fetch.
+ user_org_data (dict): The user's organization data.
+ check_owner (bool, optional): Whether to check if the user is the owner of the app. Defaults to False.
+
+ Returns:
+ dict: The fetched app.
+
+ Raises:
+ HTTPException: If the app is not found or the user does not have access to it.
+ """
+ app = await engine.find_one(AppDB, AppDB.id == ObjectId(app_id))
+ if app is None:
+ logger.error("App not found")
+ raise HTTPException
+
+ # Check user's access to the organization linked to the app.
+ organization_id = app.organization.id
+ access = await check_user_org_access(
+ user_org_data, str(organization_id), check_owner
+ )
+ if not access:
+ error_msg = f"You do not have access to this app: {app_id}"
+ raise HTTPException(status_code=403, detail=error_msg)
+ return app
+
+
+async def fetch_app_variant_and_check_access(
+ app_variant_id: str, user_org_data: dict, check_owner=False
+):
+ """
+ Fetches an app variant from the database and checks if the user has access to it.
+
+ Args:
+ app_variant_id (str): The ID of the app variant to fetch.
+ user_org_data (dict): The user's organization data.
+ check_owner (bool, optional): Whether to check if the user is the owner of the app variant. Defaults to False.
+
+ Returns:
+ AppVariantDB: The fetched app variant.
+
+ Raises:
+ HTTPException: If the app variant is not found or the user does not have access to it.
+ """
+ app_variant = await engine.find_one(
+ AppVariantDB, AppVariantDB.id == ObjectId(app_variant_id)
+ )
+ if app_variant is None:
+ logger.error("App variant not found")
+ raise HTTPException
+
+ # Check user's access to the organization linked to the app.
+ organization_id = app_variant.organization.id
+ access = await check_user_org_access(
+ user_org_data, str(organization_id), check_owner
+ )
+ if not access:
+ error_msg = f"You do not have access to this app variant: {app_variant_id}"
+ raise HTTPException(status_code=403, detail=error_msg)
+ return app_variant
+
+
+async def fetch_app_by_name_and_organization(
+ app_name: str, organization_id: str, **user_org_data: dict
+):
+ """Fetch an app by it's name and organization id.
+
+ Args:
+ app_name (str): The name of the app
+ organization_id (str): The ID of the app organization
+
+ Returns:
+ AppDB: the instance of the app
+ """
+
+ query_expression = (AppDB.app_name == app_name) & (
+ AppDB.organization == ObjectId(organization_id)
+ )
+ app_db = await engine.find_one(AppDB, query_expression)
+ return app_db
diff --git a/agenta-backend/agenta_backend/services/db_mongo.py b/agenta-backend/agenta_backend/services/db_mongo.py
deleted file mode 100644
index 6031fa14f5..0000000000
--- a/agenta-backend/agenta_backend/services/db_mongo.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from motor.motor_asyncio import AsyncIOMotorClient
-
-client = AsyncIOMotorClient("mongodb://username:password@mongo:27017")
-database = client["agenta"]
-
-evaluation_scenarios = database["evaluation_scenarios"]
-evaluations = database["evaluations"]
-testsets = database["testsets"]
diff --git a/agenta-backend/agenta_backend/services/deployment_manager.py b/agenta-backend/agenta_backend/services/deployment_manager.py
new file mode 100644
index 0000000000..8a3b71b2ad
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/deployment_manager.py
@@ -0,0 +1,140 @@
+import logging
+import os
+from typing import Dict
+
+from agenta_backend.config import settings
+from agenta_backend.models.api.api_models import Image
+from agenta_backend.models.db_models import AppVariantDB, DeploymentDB, ImageDB
+from agenta_backend.services import db_manager, docker_utils
+from docker.errors import DockerException
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+async def start_service(
+ app_variant_db: AppVariantDB, env_vars: Dict[str, str]
+) -> DeploymentDB:
+ """
+ Start a service.
+
+ Args:
+ image_name: List of image tags.
+ app_name: Name of the app.
+ base_name: Base name for the container.
+ env_vars: Environment variables.
+ organization_id: ID of the organization.
+
+ Returns:
+ True if successful, False otherwise.
+ """
+
+ uri_path = f"{app_variant_db.organization.id}/{app_variant_db.app.app_name}/{app_variant_db.base_name}"
+ container_name = f"{app_variant_db.app.app_name}-{app_variant_db.base_name}-{app_variant_db.organization.id}"
+ logger.debug("Starting service with the following parameters:")
+ logger.debug(f"image_name: {app_variant_db.image.tags}")
+ logger.debug(f"uri_path: {uri_path}")
+ logger.debug(f"container_name: {container_name}")
+ logger.debug(f"env_vars: {env_vars}")
+
+ results = docker_utils.start_container(
+ image_name=app_variant_db.image.tags,
+ uri_path=uri_path,
+ container_name=container_name,
+ env_vars=env_vars,
+ )
+ uri = results["uri"]
+ container_id = results["container_id"]
+ container_name = results["container_name"]
+
+ logger.info(
+ f"Started Docker container for app variant {app_variant_db.app.app_name}/{app_variant_db.variant_name} at URI {uri}"
+ )
+
+ deployment = await db_manager.create_deployment(
+ app=app_variant_db.app,
+ organization=app_variant_db.organization,
+ user=app_variant_db.user,
+ container_name=container_name,
+ container_id=container_id,
+ uri=uri,
+ status="running",
+ )
+ return deployment
+
+
+async def remove_image(image: Image):
+ """
+ Remove a Docker image from the system.
+
+ Args:
+ image (Image): The Docker image to remove.
+
+ Returns:
+ None
+ """
+ try:
+ if os.environ["FEATURE_FLAG"] not in ["cloud", "ee"] and image.deletable:
+ docker_utils.delete_image(image.docker_id)
+ logger.info(f"Image {image.docker_id} deleted")
+ except RuntimeError as e:
+ logger.error(f"Error deleting image {image.docker_id}: {e}")
+ raise e
+
+
+async def stop_service(deployment: DeploymentDB):
+ """
+ Stops the Docker container associated with the given deployment.
+
+ Args:
+ deployment (DeploymentDB): The deployment to stop.
+
+ Returns:
+ None
+ """
+ docker_utils.delete_container(deployment.container_id)
+ logger.info(f"Container {deployment.container_id} deleted")
+
+
+async def stop_and_delete_service(deployment: DeploymentDB):
+ """
+ Stop and delete a Docker container associated with a deployment.
+
+ Args:
+ deployment (DeploymentDB): The deployment object associated with the container.
+
+ Returns:
+ None
+ """
+ logger.debug(f"Stopping container {deployment.container_id}")
+ container_id = deployment.container_id
+ docker_utils.stop_container(container_id)
+ logger.info(f"Container {container_id} stopped")
+ docker_utils.delete_container(container_id)
+ logger.info(f"Container {container_id} deleted")
+
+
+async def validate_image(image: Image) -> bool:
+ """
+ Validates the given image by checking if it has tags, if the tags start with the registry name, and if the image exists in the list of Docker images.
+
+ Args:
+ image (Image): The image to be validated.
+
+ Raises:
+ ValueError: If the image tags are empty or do not start with the registry name.
+ DockerException: If the image does not exist in the list of Docker images.
+ """
+ if image.tags in ["", None]:
+ msg = "Image tags cannot be empty"
+ logger.error(msg)
+ raise ValueError(msg)
+ if not image.tags.startswith(settings.registry):
+ raise ValueError(
+ "Image should have a tag starting with the registry name (agenta-server)"
+ )
+ if image not in docker_utils.list_images():
+ raise DockerException(
+ f"Image {image.docker_id} with tags {image.tags} not found"
+ )
+ return True
diff --git a/agenta-backend/agenta_backend/services/docker_utils.py b/agenta-backend/agenta_backend/services/docker_utils.py
index fa0d466eb0..c890ba5800 100644
--- a/agenta-backend/agenta_backend/services/docker_utils.py
+++ b/agenta-backend/agenta_backend/services/docker_utils.py
@@ -6,17 +6,16 @@
import docker
from agenta_backend.config import settings
from agenta_backend.models.api.api_models import (
- URI,
- AppVariant,
Image,
DockerEnvVars,
+ Dict,
)
client = docker.from_env()
# Set up logging
-logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
def port_generator(start_port=9000):
@@ -29,6 +28,22 @@ def port_generator(start_port=9000):
ports = port_generator()
+def find_image_by_docker_id(docker_id: str) -> Image:
+ """Finds an image based on its docker id
+
+ Arguments:
+ docker_id -- _description_
+
+ Returns:
+ Image -- _description_
+ """
+ all_images = client.images.list()
+ for image in all_images:
+ if image.id == docker_id:
+ return Image(type="image", docker_id=image.id, tags=image.tags[0])
+ return None
+
+
def list_images() -> List[Image]:
"""Lists all the images from our repository
These are tagged with the registry name (config in both agenta_backend and agenta-cli)
@@ -38,44 +53,64 @@ def list_images() -> List[Image]:
"""
all_images = client.images.list()
registry_images = [
- Image(docker_id=image.id, tags=image.tags[0])
+ Image(type="image", docker_id=image.id, tags=image.tags[0])
for image in all_images
if len(image.tags) > 0 and image.tags[0].startswith(settings.registry)
]
return registry_images
-def start_container(image_name, app_name, variant_name, env_vars: DockerEnvVars) -> URI:
+def start_container(
+ image_name: str, uri_path: str, container_name: str, env_vars: DockerEnvVars
+) -> Dict:
+ logger.debug("Starting container with the following parameters:")
+ logger.debug(f"image_name: {image_name}")
+ logger.debug(f"uri_path: {uri_path}")
+ logger.debug(f"container_name: {container_name}")
+ logger.debug(f"env_vars: {env_vars}")
try:
image = client.images.get(f"{image_name}")
+ # Default labels
labels = {
- f"traefik.http.routers.{app_name}-{variant_name}.entrypoints": "web",
- f"traefik.http.services.{app_name}-{variant_name}.loadbalancer.server.port": "80",
- f"traefik.http.middlewares.{app_name}-{variant_name}-strip-prefix.stripprefix.prefixes": f"/{app_name}/{variant_name}",
- f"traefik.http.routers.{app_name}-{variant_name}.middlewares": f"{app_name}-{variant_name}-strip-prefix",
- f"traefik.http.routers.{app_name}-{variant_name}.service": f"{app_name}-{variant_name}",
+ f"traefik.http.services.{container_name}.loadbalancer.server.port": "80",
+ f"traefik.http.middlewares.{container_name}-strip-prefix.stripprefix.prefixes": f"/{uri_path}",
+ f"traefik.http.routers.{container_name}.middlewares": f"{container_name}-strip-prefix",
+ f"traefik.http.routers.{container_name}.service": f"{container_name}",
}
- rules = {
- "development": f"PathPrefix(`/{app_name}/{variant_name}`)",
- "production": f"Host(`{os.environ['BARE_DOMAIN_NAME']}`) && PathPrefix(`/{app_name}/{variant_name}`)",
- }
-
- labels.update(
- {
- f"traefik.http.routers.{app_name}-{variant_name}.rule": rules[
- os.environ["ENVIRONMENT"]
- ]
+ # Merge the default labels with environment-specific labels
+ if os.environ["ENVIRONMENT"] == "production":
+ # Production specific labels
+ production_labels = {
+ f"traefik.http.routers.{container_name}.rule": f"Host(`{os.environ['BARE_DOMAIN_NAME']}`) && PathPrefix(`/{uri_path}`)",
}
- )
+ labels.update(production_labels)
+
+ if "https" in os.environ["DOMAIN_NAME"]:
+ # SSL specific labels
+ ssl_labels = {
+ f"traefik.http.routers.{container_name}.entrypoints": "web-secure",
+ f"traefik.http.routers.{container_name}.tls": "true",
+ f"traefik.http.routers.{container_name}.tls.certresolver": "myResolver",
+ }
+ labels.update(ssl_labels)
+ else:
+ # Development specific labels
+ development_labels = {
+ f"traefik.http.routers.{container_name}.rule": f"PathPrefix(`/{uri_path}`)",
+ f"traefik.http.routers.{container_name}.entrypoints": "web",
+ }
+
+ labels.update(development_labels)
+
env_vars = {} if env_vars is None else env_vars
container = client.containers.run(
image,
detach=True,
labels=labels,
network="agenta-network",
- name=f"{app_name}-{variant_name}",
+ name=container_name,
environment=env_vars,
)
# Check the container's status
@@ -84,34 +119,66 @@ def start_container(image_name, app_name, variant_name, env_vars: DockerEnvVars)
if container.status == "exited":
logs = container.logs().decode("utf-8")
raise Exception(f"Container exited immediately. Docker Logs: {logs}")
- return URI(
- uri=f"http://{os.environ['BARE_DOMAIN_NAME']}/{app_name}/{variant_name}"
- )
+ return {
+ "uri": f"{os.environ['DOMAIN_NAME']}/{uri_path}",
+ "container_id": container.id,
+ "container_name": container_name,
+ }
except docker.errors.APIError as error:
# Container failed to run, get the logs
try:
- failed_container = client.containers.get(f"{app_name}-{variant_name}")
+ failed_container = client.containers.get(container_name)
logs = failed_container.logs().decode("utf-8")
raise Exception(f"Docker Logs: {logs}") from error
except Exception as e:
- return f"Failed to fetch logs: {str(e)} \n Exception Error: {str(error)}"
+ logger.error(
+ f"Failed to fetch logs: {str(e)} \n Exception Error: {str(error)}"
+ )
+ return None
+
+def restart_container(container_id: str):
+ """Restarts a container based on its id
-def stop_containers_based_on_image(image: Image) -> List[str]:
- """Stops all the containers that use a certain image
+ Arguments:
+ container_id -- the docker container id
+ """
+ try:
+ logger.info(f"Restarting container with id: {container_id}")
+
+ # Find the container and network by name and id
+ container = client.containers.get(container_id)
+ network = client.networks.get("agenta-network")
+
+ # Connect and restart container
+ network.connect(container)
+ container.restart()
+
+ logger.info(f"Restarted container with id: {container_id}")
+ except (docker.errors.APIError, Exception) as ex:
+ print("Err type ---> ", ex)
+ logger.error(
+ f"Error restarting container with id: {container.id}. Error: {str(ex)}"
+ )
+ raise RuntimeError(f"Error starting container with id: {container.id}") from ex
+
+
+def stop_containers_based_on_image_id(docker_id: str) -> List[str]:
+ """Stops all the containers that use a certain Docker image.
Arguments:
- image -- Image containing the docker id
+ docker_id: The ID of the Docker image.
+
+ Returns:
+ A list of the IDs of the stopped containers.
Raises:
- RuntimeError: _description_
+ RuntimeError: If there is an error stopping a container.
- Returns:
- The container ids of the stopped containers
"""
stopped_container_ids = []
for container in client.containers.list(all=True):
- if container.image.id == image.docker_id:
+ if container.image.id == docker_id:
try:
container.stop()
stopped_container_ids.append(container.id)
@@ -140,9 +207,9 @@ def stop_container(container_id: str):
logger.info(f"Stopped container with id: {container.id}")
except docker.errors.APIError as ex:
logger.error(
- f"Error stopping container with id: {container.id}. Error: {str(ex)}"
+ f"Error stopping container with id: {container_id}. Error: {str(ex)}"
)
- raise RuntimeError(f"Error stopping container with id: {container.id}") from ex
+ raise RuntimeError(f"Error stopping container with id: {container_id}") from ex
def delete_container(container_id: str):
@@ -160,12 +227,12 @@ def delete_container(container_id: str):
logger.info(f"Deleted container with id: {container.id}")
except docker.errors.APIError as ex:
logger.error(
- f"Error deleting container with id: {container.id}. Error: {str(ex)}"
+ f"Error deleting container with id: {container_id}. Error: {str(ex)}"
)
- raise RuntimeError(f"Error deleting container with id: {container.id}") from ex
+ raise RuntimeError(f"Error deleting container with id: {container_id}") from ex
-def delete_image(image: Image):
+def delete_image(docker_id: str):
"""Delete an image based on its id
Arguments:
@@ -175,13 +242,11 @@ def delete_image(image: Image):
RuntimeError: _description_
"""
try:
- client.images.remove(image.docker_id)
- logger.info(f"Deleted image with id: {image.docker_id}")
+ client.images.remove(image=docker_id)
+ logger.info(f"Deleted image with id: {docker_id}")
except docker.errors.APIError as ex:
- logger.error(
- f"Error deleting image with id: {image.docker_id}. Error: {str(ex)}"
- )
- raise RuntimeError(f"Error deleting image with id: {image.docker_id}") from ex
+ logger.error(f"Error deleting image with id: {docker_id}. Error: {str(ex)}")
+ raise RuntimeError(f"Error deleting image with id: {docker_id}") from ex
def experimental_pull_image(image_name: str):
@@ -201,7 +266,9 @@ def experimental_pull_image(image_name: str):
image = client.images.pull(image_name)
return image
except docker.errors.APIError as e:
- raise RuntimeError(f"An error occurred while pulling the image: {str(e)}")
+ raise RuntimeError(
+ f"An error occurred while pulling the image: {str(e)}"
+ ) from e
def experimental_is_image_pulled(image_name: str) -> bool:
diff --git a/agenta-backend/agenta_backend/services/evaluation_service.py b/agenta-backend/agenta_backend/services/evaluation_service.py
index 048146dd69..996e50970b 100644
--- a/agenta-backend/agenta_backend/services/evaluation_service.py
+++ b/agenta-backend/agenta_backend/services/evaluation_service.py
@@ -1,24 +1,46 @@
-from typing import Dict
+import logging
+from agenta_backend.services.security.sandbox import execute_code_safely
+from bson import ObjectId
from datetime import datetime
+from typing import Dict, List, Any, Optional
+
+from fastapi import HTTPException
+
from agenta_backend.models.api.evaluation_model import (
+ CustomEvaluationNames,
Evaluation,
+ EvaluationScenario,
+ CustomEvaluationOutput,
+ CustomEvaluationDetail,
EvaluationType,
NewEvaluation,
EvaluationScenarioUpdate,
- EvaluationStatus,
+ CreateCustomEvaluation,
+ EvaluationUpdate,
)
-from fastapi import HTTPException
-from bson import ObjectId
-from agenta_backend.services.db_mongo import (
- evaluations,
- evaluation_scenarios,
- testsets,
+from agenta_backend.models import converters
+from agenta_backend.utils.common import engine, check_access_to_app
+from agenta_backend.services.db_manager import query, get_user
+from agenta_backend.services import db_manager
+from agenta_backend.models.db_models import (
+ AppVariantDB,
+ EvaluationDB,
+ EvaluationScenarioDB,
+ UserDB,
+ AppDB,
+ EvaluationTypeSettings,
+ EvaluationScenarioInput,
+ EvaluationScenarioOutput,
+ CustomEvaluationDB,
)
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
class UpdateEvaluationScenarioError(Exception):
"""Custom exception for update evaluation scenario errors."""
@@ -26,132 +48,441 @@ class UpdateEvaluationScenarioError(Exception):
pass
-async def create_new_evaluation(newEvaluationData: NewEvaluation) -> Dict:
- evaluation = newEvaluationData.dict()
- evaluation["created_at"] = evaluation["updated_at"] = datetime.utcnow()
+async def _fetch_evaluation_and_check_access(
+ evaluation_id: str, **user_org_data: dict
+) -> EvaluationDB:
+ # Fetch the evaluation by ID
+ evaluation = await db_manager.fetch_evaluation_by_id(evaluation_id=evaluation_id)
+
+ # Check if the evaluation exists
+ if evaluation is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Evaluation with id {evaluation_id} not found",
+ )
+
+ # Check for access rights
+ access = await check_access_to_app(
+ user_org_data=user_org_data, app_id=evaluation.app.id
+ )
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {str(evaluation.app.id)}",
+ )
+ return evaluation
+
+
+async def _fetch_evaluation_scenario_and_check_access(
+ evaluation_scenario_id: str, **user_org_data: dict
+) -> EvaluationDB:
+ # Fetch the evaluation by ID
+ evaluation_scenario = await db_manager.fetch_evaluation_scenario_by_id(
+ evaluation_scenario_id=evaluation_scenario_id
+ )
+ if evaluation_scenario is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Evaluation scenario with id {evaluation_scenario_id} not found",
+ )
+ evaluation = evaluation_scenario.evaluation
+
+ # Check if the evaluation exists
+ if evaluation is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"Evaluation scenario for evaluation scenario with id {evaluation_scenario_id} not found",
+ )
+
+ # Check for access rights
+ access = await check_access_to_app(
+ user_org_data=user_org_data, app_id=evaluation.app.id
+ )
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {str(evaluation.app.id)}",
+ )
+ return evaluation_scenario
+
+
+async def create_new_evaluation(
+ payload: NewEvaluation, **user_org_data: dict
+) -> EvaluationDB:
+ """
+ Create a new evaluation based on the provided payload and additional arguments.
+
+ Args:
+ payload (NewEvaluation): The evaluation payload.
+ **user_org_data (dict): Additional keyword arguments, e.g., user id.
+
+ Returns:
+ EvaluationDB
+ """
+ user = await get_user(user_uid=user_org_data["uid"])
+
+ # Initialize evaluation type settings
+ settings = payload.evaluation_type_settings
+ evaluation_type_settings = EvaluationTypeSettings(
+ similarity_threshold=settings.similarity_threshold or 0.0,
+ regex_pattern=settings.regex_pattern or "",
+ regex_should_match=settings.regex_should_match or True,
+ webhook_url=settings.webhook_url or "",
+ custom_code_evaluation_id=settings.custom_code_evaluation_id or "",
+ llm_app_prompt_template=settings.llm_app_prompt_template or "",
+ )
- newEvaluation = await evaluations.insert_one(evaluation)
+ current_time = datetime.utcnow()
- if not newEvaluation.acknowledged:
+ # Fetch app
+ app = await db_manager.fetch_app_by_id(app_id=payload.app_id)
+ if app is None:
+ raise HTTPException(
+ status_code=404,
+ detail=f"App with id {payload.app_id} does not exist",
+ )
+
+ variants = [ObjectId(variant_id) for variant_id in payload.variant_ids]
+
+ testset = await db_manager.fetch_testset_by_id(testset_id=payload.testset_id)
+ # Initialize and save evaluation instance to database
+ eval_instance = EvaluationDB(
+ app=app,
+ organization=app.organization, # Assuming user has an organization_id attribute
+ user=user,
+ status=payload.status,
+ evaluation_type=payload.evaluation_type,
+ evaluation_type_settings=evaluation_type_settings,
+ variants=variants,
+ testset=testset,
+ created_at=current_time,
+ updated_at=current_time,
+ )
+ newEvaluation = await engine.save(eval_instance)
+
+ if newEvaluation is None:
raise HTTPException(
status_code=500, detail="Failed to create evaluation_scenario"
)
- testsetId = evaluation["testset"]["_id"]
- testset = await testsets.find_one({"_id": ObjectId(testsetId)})
- csvdata = testset["csvdata"]
+ await prepare_csvdata_and_create_evaluation_scenario(
+ testset.csvdata,
+ payload.inputs,
+ payload.evaluation_type,
+ newEvaluation,
+ user,
+ app,
+ )
+ return newEvaluation
+
+
+async def prepare_csvdata_and_create_evaluation_scenario(
+ csvdata: List[Dict[str, str]],
+ payload_inputs: List[str],
+ evaluation_type: EvaluationType,
+ new_evaluation: EvaluationDB,
+ user: UserDB,
+ app: AppDB,
+):
+ """
+ Prepares CSV data and creates evaluation scenarios based on the inputs, evaluation
+ type, and other parameters provided.
+
+ Args:
+ csvdata: A list of dictionaries representing the CSV data.
+ inputs: A list of strings representing the names of the inputs in the variant.
+ evaluation_type: The type of evaluation
+ new_evaluation: The instance of EvaluationDB
+ user: The owner of the evaluation scenario
+ app: The app the evaluation is going to belong to
+ """
for datum in csvdata:
+ # Check whether the inputs in the test set match the inputs in the variant
try:
inputs = [
{"input_name": name, "input_value": datum[name]}
- for name in evaluation["inputs"]
+ for name in payload_inputs
]
except KeyError:
- await evaluations.delete_one({"_id": newEvaluation.inserted_id})
+ await engine.delete(new_evaluation)
msg = f"""
Columns in the test set should match the names of the inputs in the variant.
- Inputs names in variant are: {evaluation['inputs']} while
+ Inputs names in variant are: {inputs} while
columns in test set are: {[col for col in datum.keys() if col != 'correct_answer']}
"""
raise HTTPException(
status_code=400,
detail=msg,
)
-
- evaluation_scenario = {
- "evaluation_id": str(newEvaluation.inserted_id),
- "inputs": inputs,
- "outputs": [],
- "created_at": datetime.utcnow(),
- "updated_at": datetime.utcnow(),
+ # Create evaluation scenarios
+ list_of_scenario_input = []
+ for scenario_input in inputs:
+ eval_scenario_input_instance = EvaluationScenarioInput(
+ input_name=scenario_input["input_name"],
+ input_value=scenario_input["input_value"],
+ )
+ list_of_scenario_input.append(eval_scenario_input_instance)
+
+ evaluation_scenario_payload = {
+ **{
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow(),
+ },
+ **_extend_with_evaluation(evaluation_type),
+ **_extend_with_correct_answer(evaluation_type, datum),
}
- evaluation_scenario = {
- **evaluation_scenario,
- **extend_with_evaluation(newEvaluationData.evaluation_type),
- **extend_with_correct_answer(newEvaluationData.evaluation_type, datum),
- }
+ eval_scenario_instance = EvaluationScenarioDB(
+ **evaluation_scenario_payload,
+ user=user,
+ organization=app.organization,
+ evaluation=new_evaluation,
+ inputs=list_of_scenario_input,
+ outputs=[],
+ )
+ await engine.save(eval_scenario_instance)
- await evaluation_scenarios.insert_one(evaluation_scenario)
- evaluation["id"] = str(newEvaluation.inserted_id)
- return evaluation
+async def create_evaluation_scenario(
+ evaluation_id: str, payload: EvaluationScenario, **user_org_data: dict
+) -> None:
+ """
+ Create a new evaluation scenario.
+ Args:
+ evaluation_id (str): The ID of the evaluation.
+ payload (EvaluationScenario): Evaluation scenario data.
+ user_org_data (dict): User and organization data.
-async def update_evaluation_status(
- evaluation_id: str, status: EvaluationStatus
-) -> Evaluation:
- result = await evaluations.update_one(
- {"_id": ObjectId(evaluation_id)}, {"$set": {"status": status.value}}
+ Raises:
+ HTTPException: If evaluation not found or access denied.
+ """
+ evaluation = await _fetch_evaluation_and_check_access(
+ evaluation_id=evaluation_id, **user_org_data
)
- if result.acknowledged:
- doc = await evaluations.find_one({"_id": ObjectId(evaluation_id)})
- if doc:
- doc["id"] = str(doc["_id"])
- del doc["_id"]
- return Evaluation(**doc)
- else:
- raise UpdateEvaluationScenarioError("Failed to update evaluation status")
+
+ scenario_inputs = [
+ EvaluationScenarioInput(
+ input_name=input_item.input_name,
+ input_value=input_item.input_value,
+ )
+ for input_item in payload.inputs
+ ]
+
+ new_eval_scenario = EvaluationScenarioDB(
+ user=evaluation.user,
+ organization=evaluation.organization,
+ evaluation=evaluation,
+ inputs=scenario_inputs,
+ outputs=[],
+ is_pinned=False,
+ note="",
+ **_extend_with_evaluation(evaluation.evaluation_type),
+ created_at=datetime.utcnow(),
+ updated_at=datetime.utcnow(),
+ )
+
+ await engine.save(new_eval_scenario)
+
+
+async def update_evaluation(
+ evaluation_id: str, update_payload: EvaluationUpdate, **user_org_data: dict
+) -> None:
+ """
+ Update an existing evaluation based on the provided payload.
+
+ Args:
+ evaluation_id (str): The existing evaluation ID.
+ update_payload (EvaluationUpdate): The payload for the update.
+
+ Raises:
+ HTTPException: If the evaluation is not found or access is denied.
+ """
+ # Fetch the evaluation by ID
+ evaluation = await _fetch_evaluation_and_check_access(
+ evaluation_id=evaluation_id,
+ **user_org_data,
+ )
+
+ # Prepare updates
+ updates = {}
+ if update_payload.status is not None:
+ updates["status"] = update_payload.status
+
+ if update_payload.evaluation_type_settings is not None:
+ current_settings = evaluation.evaluation_type_settings
+ new_settings = update_payload.evaluation_type_settings
+
+ # Update only the fields that are explicitly set in the payload
+ for field in EvaluationTypeSettings.__annotations__.keys():
+ setattr(
+ current_settings,
+ field,
+ getattr(new_settings, field, None)
+ or getattr(current_settings, field, None),
+ )
+
+ updates["evaluation_type_settings"] = current_settings
+
+ # Update the evaluation
+ evaluation.update(updates)
+ await engine.save(evaluation)
+
+
+async def fetch_evaluation_scenarios_for_evaluation(
+ evaluation_id: str, **user_org_data: dict
+) -> List[EvaluationScenario]:
+ """
+ Fetch evaluation scenarios for a given evaluation ID.
+
+ Args:
+ evaluation_id (str): The ID of the evaluation.
+ user_org_data (dict): User and organization data.
+
+ Raises:
+ HTTPException: If the evaluation is not found or access is denied.
+
+ Returns:
+ List[EvaluationScenario]: A list of evaluation scenarios.
+ """
+ evaluation = await _fetch_evaluation_and_check_access(
+ evaluation_id=evaluation_id,
+ **user_org_data,
+ )
+ scenarios = await engine.find(
+ EvaluationScenarioDB,
+ EvaluationScenarioDB.evaluation == ObjectId(evaluation.id),
+ )
+ eval_scenarios = [
+ converters.evaluation_scenario_db_to_pydantic(scenario)
+ for scenario in scenarios
+ ]
+ return eval_scenarios
async def update_evaluation_scenario(
evaluation_scenario_id: str,
evaluation_scenario_data: EvaluationScenarioUpdate,
evaluation_type: EvaluationType,
-) -> Dict:
- evaluation_scenario_dict = evaluation_scenario_data.dict()
- evaluation_scenario_dict["updated_at"] = datetime.utcnow()
+ **user_org_data,
+) -> None:
+ """
+ Updates an evaluation scenario.
- new_evaluation_set = {"outputs": evaluation_scenario_dict["outputs"]}
+ Args:
+ evaluation_scenario_id (str): The ID of the evaluation scenario.
+ evaluation_scenario_data (EvaluationScenarioUpdate): New data for the scenario.
+ evaluation_type (EvaluationType): Type of the evaluation.
+ user_org_data (dict): User and organization data.
- if (
- evaluation_type == EvaluationType.auto_exact_match
- or evaluation_type == EvaluationType.auto_similarity_match
- ):
- new_evaluation_set["score"] = evaluation_scenario_dict["score"]
- elif evaluation_type == EvaluationType.human_a_b_testing:
- new_evaluation_set["vote"] = evaluation_scenario_dict["vote"]
- elif evaluation_type == EvaluationType.auto_ai_critique:
- current_evaluation_scenario = await evaluation_scenarios.find_one(
- {"_id": ObjectId(evaluation_scenario_id)}
- )
- current_evaluation = await evaluations.find_one(
- {"_id": ObjectId(current_evaluation_scenario["evaluation_id"])}
- )
+ Raises:
+ HTTPException: If evaluation scenario not found or access denied.
+ """
+ eval_scenario = await _fetch_evaluation_scenario_and_check_access(
+ evaluation_scenario_id=evaluation_scenario_id,
+ **user_org_data,
+ )
- evaluation = evaluate_with_ai_critique(
- llm_app_prompt_template=current_evaluation["llm_app_prompt_template"],
- llm_app_inputs=current_evaluation_scenario["inputs"],
- correct_answer=current_evaluation_scenario["correct_answer"],
- app_variant_output=new_evaluation_set["outputs"][0]["variant_output"],
- evaluation_prompt_template=evaluation_scenario_dict[
- "evaluation_prompt_template"
- ],
- open_ai_key=evaluation_scenario_dict["open_ai_key"],
- )
+ updated_data = evaluation_scenario_data.dict()
+ updated_data["updated_at"] = datetime.utcnow()
+ new_eval_set = {}
+
+ if evaluation_type in [
+ EvaluationType.auto_exact_match,
+ EvaluationType.auto_similarity_match,
+ EvaluationType.auto_regex_test,
+ EvaluationType.auto_webhook_test,
+ EvaluationType.auto_ai_critique,
+ EvaluationType.single_model_test,
+ ]:
+ new_eval_set["score"] = updated_data["score"]
+ elif evaluation_type == EvaluationType.human_a_b_testing:
+ new_eval_set["vote"] = updated_data["vote"]
+ elif evaluation_type == EvaluationType.custom_code_run:
+ new_eval_set["correct_answer"] = updated_data["correct_answer"]
+
+ if updated_data["outputs"] is not None:
+ new_outputs = [
+ EvaluationScenarioOutput(
+ variant_id=output["variant_id"],
+ variant_output=output["variant_output"],
+ ).dict()
+ for output in updated_data["outputs"]
+ ]
+ new_eval_set["outputs"] = new_outputs
+
+ if updated_data["inputs"] is not None:
+ new_inputs = [
+ EvaluationScenarioInput(
+ input_name=input_item["input_name"],
+ input_value=input_item["input_value"],
+ ).dict()
+ for input_item in updated_data["inputs"]
+ ]
+ new_eval_set["inputs"] = new_inputs
+
+ if updated_data["is_pinned"] is not None:
+ new_eval_set["is_pinned"] = updated_data["is_pinned"]
+
+ if updated_data["note"] is not None:
+ new_eval_set["note"] = updated_data["note"]
+
+ if updated_data["correct_answer"] is not None:
+ new_eval_set["correct_answer"] = updated_data["correct_answer"]
+
+ eval_scenario.update(new_eval_set)
+ await engine.save(eval_scenario)
+
+
+async def update_evaluation_scenario_score(
+ evaluation_scenario_id: str, score: float, **user_org_data: dict
+) -> None:
+ """
+ Updates the score of an evaluation scenario.
- new_evaluation_set["evaluation"] = evaluation
+ Args:
+ evaluation_scenario_id (str): The ID of the evaluation scenario.
+ score (float): The new score to set.
+ user_org_data (dict): User and organization data.
- result = await evaluation_scenarios.update_one(
- {"_id": ObjectId(evaluation_scenario_id)}, {"$set": new_evaluation_set}
+ Raises:
+ HTTPException: If evaluation scenario not found or access denied.
+ """
+ eval_scenario = await _fetch_evaluation_scenario_and_check_access(
+ evaluation_scenario_id, **user_org_data
)
- if result.acknowledged:
- evaluation_scenario = await evaluation_scenarios.find_one(
- {"_id": ObjectId(evaluation_scenario_id)}
- )
+ eval_scenario.score = score
- if evaluation_scenario:
- evaluation_scenario["id"] = str(evaluation_scenario["_id"])
- del evaluation_scenario["_id"]
- return evaluation_scenario
+ # Save the updated evaluation scenario
+ await engine.save(eval_scenario)
- raise UpdateEvaluationScenarioError("Failed to create evaluation_scenario")
+
+async def get_evaluation_scenario_score(
+ evaluation_scenario_id: str, **user_org_data: dict
+) -> Dict[str, str]:
+ """
+ Retrieve the score of a given evaluation scenario.
+
+ Args:
+ evaluation_scenario_id: The ID of the evaluation scenario.
+ user_org_data: Additional user and organization data.
+
+ Returns:
+ Dictionary with 'scenario_id' and 'score' keys.
+ """
+ evaluation_scenario = await _fetch_evaluation_scenario_and_check_access(
+ evaluation_scenario_id, **user_org_data
+ )
+ return {
+ "scenario_id": str(evaluation_scenario.id),
+ "score": evaluation_scenario.score,
+ }
def evaluate_with_ai_critique(
llm_app_prompt_template: str,
- llm_app_inputs: dict,
+ llm_app_inputs: list,
correct_answer: str,
app_variant_output: str,
evaluation_prompt_template: str,
@@ -166,7 +497,7 @@ def evaluate_with_ai_critique(
Args:
llm_app_prompt_template (str): the prompt template of the llm app variant
- llm_app_inputs (dict): parameters
+ llm_app_inputs (list): parameters
correct_answer (str): correct answer
app_variant_output (str): the output of an ll app variant with given parameters
evaluation_prompt_template (str): evaluation prompt set by an agenta user in the ai evaluation view
@@ -179,7 +510,11 @@ def evaluate_with_ai_critique(
input_variables = []
# List of default variables
- default_vars = ["app_variant_output", "llm_app_prompt_template", "correct_answer"]
+ default_vars = [
+ "app_variant_output",
+ "llm_app_prompt_template",
+ "correct_answer",
+ ]
# Check default variables
for var in default_vars:
@@ -209,29 +544,373 @@ def evaluate_with_ai_critique(
return output.strip()
-def extend_with_evaluation(evaluation_type: EvaluationType):
+def _extend_with_evaluation(evaluation_type: EvaluationType):
evaluation = {}
if (
evaluation_type == EvaluationType.auto_exact_match
or evaluation_type == EvaluationType.auto_similarity_match
+ or evaluation_type == EvaluationType.auto_regex_test
+ or evaluation_type == EvaluationType.auto_webhook_test
+ or evaluation_type == EvaluationType.single_model_test
+ or EvaluationType.auto_ai_critique
):
evaluation["score"] = ""
if evaluation_type == EvaluationType.human_a_b_testing:
evaluation["vote"] = ""
-
- if evaluation_type == EvaluationType.auto_ai_critique:
- evaluation["evaluation"] = ""
return evaluation
-def extend_with_correct_answer(evaluation_type: EvaluationType, row: dict):
+def _extend_with_correct_answer(evaluation_type: EvaluationType, row: dict):
correct_answer = {}
if (
evaluation_type == EvaluationType.auto_exact_match
or evaluation_type == EvaluationType.auto_similarity_match
+ or evaluation_type == EvaluationType.auto_regex_test
or evaluation_type == EvaluationType.auto_ai_critique
+ or evaluation_type == EvaluationType.auto_webhook_test
):
if row["correct_answer"]:
correct_answer["correct_answer"] = row["correct_answer"]
return correct_answer
+
+
+async def fetch_list_evaluations(
+ app_id: str,
+ **user_org_data: dict,
+) -> List[Evaluation]:
+ """
+ Fetches a list of evaluations based on the provided filtering criteria.
+
+ Args:
+ app_id (Optional[str]): An optional app ID to filter the evaluations.
+ user_org_data (dict): User and organization data.
+
+ Returns:
+ List[Evaluation]: A list of evaluations.
+ """
+ access = await check_access_to_app(user_org_data=user_org_data, app_id=app_id)
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {app_id}",
+ )
+
+ evaluations_db = await engine.find(
+ EvaluationDB, EvaluationDB.app == ObjectId(app_id)
+ )
+ return [
+ await converters.evaluation_db_to_pydantic(evaluation)
+ for evaluation in evaluations_db
+ ]
+
+
+async def fetch_evaluation(evaluation_id: str, **user_org_data: dict) -> Evaluation:
+ """
+ Fetches a single evaluation based on its ID.
+
+ Args:
+ evaluation_id (str): The ID of the evaluation.
+ user_org_data (dict): User and organization data.
+
+ Returns:
+ Evaluation: The fetched evaluation.
+ """
+ evaluation = await _fetch_evaluation_and_check_access(
+ evaluation_id=evaluation_id, **user_org_data
+ )
+ return await converters.evaluation_db_to_pydantic(evaluation)
+
+
+async def delete_evaluations(evaluation_ids: List[str], **user_org_data: dict) -> None:
+ """
+ Delete evaluations by their IDs.
+
+ Args:
+ evaluation_ids (List[str]): A list of evaluation IDs.
+ user_org_data (dict): User and organization data.
+
+ Raises:
+ HTTPException: If evaluation not found or access denied.
+ """
+ for evaluation_id in evaluation_ids:
+ evaluation = await _fetch_evaluation_and_check_access(
+ evaluation_id=evaluation_id, **user_org_data
+ )
+ await engine.delete(evaluation)
+
+
+async def create_custom_code_evaluation(
+ payload: CreateCustomEvaluation, **user_org_data: dict
+) -> str:
+ """Save the custom evaluation code in the database.
+
+ Args:
+ payload (CreateCustomEvaluation): the required payload
+
+ Returns:
+ str: the custom evaluation id
+ """
+
+ # Initialize custom evaluation instance
+ access = await check_access_to_app(
+ user_org_data=user_org_data, app_id=payload.app_id
+ )
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {payload.app_id}",
+ )
+ app = await db_manager.fetch_app_by_id(app_id=payload.app_id)
+ custom_eval = CustomEvaluationDB(
+ evaluation_name=payload.evaluation_name,
+ user=app.user,
+ organization=app.organization,
+ app=app,
+ python_code=payload.python_code,
+ created_at=datetime.utcnow(),
+ updated_at=datetime.utcnow(),
+ )
+
+ await engine.save(custom_eval)
+ return str(custom_eval.id)
+
+
+async def update_custom_code_evaluation(
+ id: str, payload: CreateCustomEvaluation, **kwargs: dict
+) -> str:
+ """Update a custom code evaluation in the database.
+ Args:
+ id (str): the ID of the custom evaluation to update
+ payload (CreateCustomEvaluation): the payload with updated data
+ Returns:
+ str: the ID of the updated custom evaluation
+ """
+
+ # Get user object
+ user = await get_user(user_uid=kwargs["uid"])
+
+ # Build query expression
+ query_expression = query.eq(CustomEvaluationDB.user, user.id) & query.eq(
+ CustomEvaluationDB.id, ObjectId(id)
+ )
+
+ # Get custom evaluation
+ custom_eval = await engine.find_one(CustomEvaluationDB, query_expression)
+ if not custom_eval:
+ raise HTTPException(status_code=404, detail="Custom evaluation not found")
+
+ # Update the custom evaluation fields
+ custom_eval.evaluation_name = payload.evaluation_name
+ custom_eval.python_code = payload.python_code
+ custom_eval.updated_at = datetime.utcnow()
+
+ # Save the updated custom evaluation
+ await engine.save(custom_eval)
+
+ return str(custom_eval.id)
+
+
+async def execute_custom_code_evaluation(
+ evaluation_id: str,
+ app_id: str,
+ output: str,
+ correct_answer: str,
+ variant_id: str,
+ inputs: Dict[str, Any],
+ **user_org_data: dict,
+):
+ """Execute the custom evaluation code.
+
+ Args:
+ evaluation_id (str): the custom evaluation id
+ app_id (str): the ID of the app
+ output (str): required by the custom code
+ correct_answer (str): required by the custom code
+ variant_id (str): required by the custom code
+ inputs (Dict[str, Any]): required by the custom code
+
+ Raises:
+ HTTPException: Evaluation not found
+ HTTPException: You do not have access to this app: {app_id}
+ HTTPException: App variant not found
+ HTTPException: Failed to execute custom code evaluation
+
+ Returns:
+ result: The result of the executed custom code
+ """
+ logger.debug(
+ f"evaluation_id {evaluation_id} | app_id {app_id} | variant_id {variant_id} | inputs {inputs} | output {output} | correct_answer {correct_answer}"
+ )
+ # Get user object
+ user = await get_user(user_uid=user_org_data["uid"])
+
+ # Build query expression
+ query_expression = query.eq(
+ CustomEvaluationDB.id, ObjectId(evaluation_id)
+ ) & query.eq(CustomEvaluationDB.user, user.id)
+
+ # Get custom evaluation
+ custom_eval = await engine.find_one(CustomEvaluationDB, query_expression)
+ if not custom_eval:
+ raise HTTPException(status_code=404, detail="Evaluation not found")
+
+ # Check if user has app access
+ access = await check_access_to_app(user_org_data=user_org_data, app_id=app_id)
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {app_id}",
+ )
+
+ # Retrieve app from database
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+
+ # Build query expression for app variant
+ appvar_query_expression = query.eq(AppVariantDB.app, app.id) & query.eq(
+ AppVariantDB.id, ObjectId(variant_id)
+ )
+
+ # Get app variant object
+ app_variant = await engine.find_one(AppVariantDB, appvar_query_expression)
+ if not app_variant:
+ raise HTTPException(status_code=404, detail="App variant not found")
+
+ # Execute the Python code with the provided inputs
+ try:
+ result = execute_code_safely(
+ app_variant.config.parameters,
+ inputs,
+ output,
+ correct_answer,
+ custom_eval.python_code,
+ )
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to execute custom code evaluation: {str(e)}",
+ )
+ return result
+
+
+async def fetch_custom_evaluations(
+ app_id: str, **user_org_data: dict
+) -> List[CustomEvaluationOutput]:
+ """Fetch a list of custom evaluations from the database.
+
+ Args:
+ app_name (str): the name of the app
+
+ Returns:
+ List[CustomEvaluationOutput]: ls=ist of custom evaluations
+ """
+ # Get user object
+ access = await check_access_to_app(user_org_data=user_org_data, app_id=app_id)
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {app_id}",
+ )
+
+ # Retrieve app from database
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+
+ # Get custom evaluations
+ custom_evals = await engine.find(
+ CustomEvaluationDB, CustomEvaluationDB.app == ObjectId(app.id)
+ )
+ if not custom_evals:
+ return []
+
+ # Convert custom evaluations to evaluations
+ evaluations = []
+ for custom_eval in custom_evals:
+ evaluations.append(
+ CustomEvaluationOutput(
+ id=str(custom_eval.id),
+ app_id=str(custom_eval.app.id),
+ evaluation_name=custom_eval.evaluation_name,
+ created_at=custom_eval.created_at,
+ )
+ )
+ return evaluations
+
+
+async def fetch_custom_evaluation_detail(
+ id: str, **user_org_data: dict
+) -> CustomEvaluationDetail:
+ """Fetch the detail of custom evaluation from the database.
+
+ Args:
+ id (str): the id of the custom evaluation
+
+ Returns:
+ CustomEvaluationDetail: Detail of the custom evaluation
+ """
+
+ # Get user object
+ user = await get_user(user_uid=user_org_data["uid"])
+
+ # Build query expression
+ query_expression = query.eq(CustomEvaluationDB.user, user.id) & query.eq(
+ CustomEvaluationDB.id, ObjectId(id)
+ )
+
+ # Get custom evaluation
+ custom_eval = await engine.find_one(CustomEvaluationDB, query_expression)
+ if not custom_eval:
+ raise HTTPException(status_code=404, detail="Custom evaluation not found")
+
+ return CustomEvaluationDetail(
+ id=str(custom_eval.id),
+ app_id=str(custom_eval.app.id),
+ python_code=custom_eval.python_code,
+ evaluation_name=custom_eval.evaluation_name,
+ created_at=custom_eval.created_at,
+ updated_at=custom_eval.updated_at,
+ )
+
+
+async def fetch_custom_evaluation_names(
+ app_id: str, **user_org_data: dict
+) -> List[CustomEvaluationNames]:
+ """Fetch the names of custom evaluation from the database.
+
+ Args:
+ id (str): the name of the app the evaluation belongs to
+
+ Returns:
+ List[CustomEvaluationNames]: the list of name of custom evaluations
+ """
+
+ # Get user object
+ user = await get_user(user_uid=user_org_data["uid"])
+
+ # Check if user has app access
+ access = await check_access_to_app(user_org_data=user_org_data, app_id=app_id)
+ if not access:
+ raise HTTPException(
+ status_code=403,
+ detail=f"You do not have access to this app: {app_id}",
+ )
+
+ # Retrieve app from database
+ app = await db_manager.fetch_app_by_id(app_id=app_id)
+
+ # Build query expression
+ query_expression = query.eq(CustomEvaluationDB.user, user.id) & query.eq(
+ CustomEvaluationDB.app, app.id
+ )
+
+ # Get custom evaluation
+ custom_evals = await engine.find(CustomEvaluationDB, query_expression)
+
+ list_of_custom_eval_names = []
+ for custom_eval in custom_evals:
+ list_of_custom_eval_names.append(
+ CustomEvaluationNames(
+ id=str(custom_eval.id),
+ evaluation_name=custom_eval.evaluation_name,
+ )
+ )
+ return list_of_custom_eval_names
diff --git a/agenta-backend/agenta_backend/services/event_db_manager.py b/agenta-backend/agenta_backend/services/event_db_manager.py
new file mode 100644
index 0000000000..34ead44c51
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/event_db_manager.py
@@ -0,0 +1,299 @@
+import logging
+from typing import List
+from bson import ObjectId
+from datetime import datetime
+
+from fastapi import HTTPException
+
+from agenta_backend.models.api.observability_models import (
+ Span,
+ CreateSpan,
+ Feedback,
+ CreateFeedback,
+ UpdateFeedback,
+ Trace,
+ CreateTrace,
+ UpdateTrace,
+)
+from agenta_backend.models.converters import (
+ spans_db_to_pydantic,
+ feedback_db_to_pydantic,
+ trace_db_to_pydantic,
+)
+from agenta_backend.services import db_manager
+from agenta_backend.models.db_models import (
+ TraceDB,
+ Feedback as FeedbackDB,
+ SpanDB,
+)
+from agenta_backend.models.db_engine import DBEngine
+
+from odmantic import query
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+
+async def get_variant_traces(
+ app_id: str, variant_id: str, **kwargs: dict
+) -> List[Trace]:
+ """Get the traces for a given app variant.
+
+ Args:
+ app_id (str): the app id of the trace
+ variant_id (str): the id of the variant
+
+ Returns:
+ List[Trace]: the list of traces for the given app variant
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+ query_expressions = (
+ query.eq(TraceDB.user, user.id)
+ & query.eq(TraceDB.app_id, app_id)
+ & query.eq(TraceDB.variant_id, variant_id)
+ )
+
+ traces = await engine.find(TraceDB, query_expressions)
+ return [trace_db_to_pydantic(trace) for trace in traces]
+
+
+async def create_app_trace(payload: CreateTrace, **kwargs: dict) -> str:
+ """Create a new trace.
+
+ Args:
+ payload (CreateTrace): the required payload
+
+ Returns:
+ Trace: the created trace
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+
+ # Ensure spans exists in the db
+ for span in payload.spans:
+ span_db = await engine.find_one(SpanDB, SpanDB.id == ObjectId(span))
+ if span_db is None:
+ raise HTTPException(404, detail=f"Span {span} does not exist")
+
+ trace = TraceDB(**payload.dict(), user=user)
+ await engine.save(trace)
+ return trace_db_to_pydantic(trace)["trace_id"]
+
+
+async def get_single_trace(trace_id: str, **kwargs: dict) -> Trace:
+ """Get a single trace.
+
+ Args:
+ trace_id (str): the Id of the trace
+
+ Returns:
+ Trace: the trace
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+ return trace_db_to_pydantic(trace)
+
+
+async def trace_status_update(
+ trace_id: str, payload: UpdateTrace, **kwargs: dict
+) -> bool:
+ """Update status of trace.
+
+ Args:
+ trace_id (str): the Id of the trace
+ payload (UpdateTrace): the required payload
+
+ Returns:
+ bool: True if successful
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+
+ # Update and save trace
+ trace.status = payload.status
+ await engine.save(trace)
+ return True
+
+
+async def create_trace_span(payload: CreateSpan, **kwargs: dict) -> str:
+ """Create a new span for a given trace.
+
+ Args:
+ payload (CreateSpan): the required payload
+
+ Returns:
+ str: the created span id
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+
+ span_db = SpanDB(**payload.dict())
+ await engine.save(span_db)
+ return str(span_db.id)
+
+
+async def get_trace_spans(trace_id: str, **kwargs: dict) -> List[Span]:
+ """Get the spans for a given trace.
+
+ Args:
+ trace_id (str): the Id of the trace
+
+ Returns:
+ List[Span]: the list of spans for the given trace
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+
+ # Get trace spans
+ spans = spans_db_to_pydantic(trace.spans)
+ return spans
+
+
+async def add_feedback_to_trace(
+ trace_id: str, payload: CreateFeedback, **kwargs: dict
+) -> str:
+ """Add a feedback to a trace.
+
+ Args:
+ trace_id (str): the Id of the trace
+ payload (CreateFeedback): the required payload
+
+ Returns:
+ str: the feedback id
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+ feedback = FeedbackDB(
+ user_id=str(user.id),
+ feedback=payload.feedback,
+ score=payload.score,
+ created_at=datetime.utcnow(),
+ )
+
+ trace = await engine.find_one(TraceDB, TraceDB.id == ObjectId(trace_id))
+ if trace.feedbacks is None:
+ trace.feedbacks = [feedback]
+ else:
+ trace.feedbacks.append(feedback)
+
+ # Update trace
+ await engine.save(trace)
+ return feedback.uid
+
+
+async def get_trace_feedbacks(trace_id: str, **kwargs: dict) -> List[Feedback]:
+ """Get the feedbacks for a given trace.
+
+ Args:
+ trace_id (str): the Id of the trace
+
+ Returns:
+ List[Feedback]: the list of feedbacks for the given trace
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+
+ # Build query expressions
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get feedbacks in trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+ feedbacks = [feedback_db_to_pydantic(feedback) for feedback in trace.feedbacks]
+ return feedbacks
+
+
+async def get_feedback_detail(
+ trace_id: str, feedback_id: str, **kwargs: dict
+) -> Feedback:
+ """Get a single feedback.
+
+ Args:
+ trace_id (str): the Id of the trace
+ feedback_id (str): the Id of the feedback
+
+ Returns:
+ Feedback: the feedback
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+
+ # Build query expressions
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+
+ # Get feedback
+ feedback = [
+ feedback_db_to_pydantic(feedback)
+ for feedback in trace.feedbacks
+ if feedback.uid == feedback_id
+ ]
+ return feedback[0]
+
+
+async def update_trace_feedback(
+ trace_id: str, feedback_id: str, payload: UpdateFeedback, **kwargs: dict
+) -> Feedback:
+ """Update a feedback.
+
+ Args:
+ trace_id (str): the Id of the trace
+ feedback_id (str): the Id of the feedback
+ payload (UpdateFeedback): the required payload
+
+ Returns:
+ Feedback: the feedback
+ """
+
+ user = await db_manager.get_user(user_uid=kwargs["uid"])
+
+ # Build query expressions
+ query_expressions = query.eq(TraceDB.id, ObjectId(trace_id)) & query.eq(
+ TraceDB.user, user.id
+ )
+
+ # Get trace
+ trace = await engine.find_one(TraceDB, query_expressions)
+
+ # update feedback
+ feedback_json = {}
+ for feedback in trace.feedbacks:
+ if feedback.uid == feedback_id:
+ feedback.update(payload.dict())
+ feedback_json = feedback.dict()
+ break
+
+ # Save feedback in trace and return a copy
+ await engine.save(trace)
+
+ # Replace key and transform into a pydantic representation
+ feedback_json["feedback_id"] = feedback_json.pop("uid")
+ return Feedback(**feedback_json)
diff --git a/agenta-backend/agenta_backend/services/helpers.py b/agenta-backend/agenta_backend/services/helpers.py
index 1c576fc1f9..321be807c7 100644
--- a/agenta-backend/agenta_backend/services/helpers.py
+++ b/agenta-backend/agenta_backend/services/helpers.py
@@ -1,16 +1,58 @@
-def print_app_variant(app_variant):
- print(f"App Variant ID: {app_variant.id}")
- print(f"App Variant Name: {app_variant.variant_name}")
- print(f"App Name: {app_variant.app_name}")
- print(f"Image ID: {app_variant.image_id}")
- print(f"Parameters: {app_variant.parameters}")
- print(f"Previous Variant Name: {app_variant.previous_variant_name}")
- print(f"Is Deleted: {app_variant.is_deleted}")
- print("------------------------")
-
-
-def print_image(image):
- print(f"Image ID: {image.id}")
- print(f"Docker ID: {image.docker_id}")
- print(f"Tags: {image.tags}")
- print("------------------------")
+import json
+from typing import List, Dict, Any
+
+
+def format_inputs(list_of_dictionaries: List[Dict[str, Any]]) -> Dict:
+ """
+ Formats a list of inputs dictionaries into a dictionary.
+
+ Args:
+ list_of_dictionaries: A list of inputs as dictionaries.
+
+ Returns:
+ A dictionary.
+ """
+
+ formatted_dictionary = {}
+ for dictionary in list_of_dictionaries:
+ formatted_dictionary[dictionary["input_name"]] = dictionary["input_value"]
+ return formatted_dictionary
+
+
+def format_outputs(list_of_dictionaries: List[Dict[str, Any]]) -> Dict:
+ """
+ Formats a list of outputs dictionaries into a dictionary.
+
+ Args:
+ list_of_dictionaries: A list of outputs as dictionaries.
+
+ Returns:
+ A dictionary.
+ """
+
+ formatted_dictionary = {}
+ for dictionary in list_of_dictionaries:
+ formatted_dictionary[dictionary["variant_id"]] = dictionary["variant_output"]
+ return formatted_dictionary
+
+
+def include_dynamic_values(json_data: Dict, inputs: Dict[str, Any]) -> Dict:
+ """
+ Includes the dynamic values in the JSON before it gets executed.
+
+ Args:
+ json_data: The JSON data.
+ inputs: The dynamic values.
+
+ Returns:
+ The modified JSON data.
+ """
+
+ # Get the inputs dictionary.
+ inputs_dictionary = json.loads(inputs)
+
+ # Replace the `{inputs}` placeholder in the JSON data with the inputs dictionary.
+ for key, value in inputs_dictionary.items():
+ json_data = json_data.replace(f"{key}", value)
+
+ return json_data
diff --git a/agenta-backend/agenta_backend/services/json_importer_helper.py b/agenta-backend/agenta_backend/services/json_importer_helper.py
new file mode 100644
index 0000000000..fdd5f54676
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/json_importer_helper.py
@@ -0,0 +1,17 @@
+import json
+
+
+def get_json(json_path: str):
+ """Reads and returns the contents of a JSON file as a list of
+ dictionaries.
+
+ Args:
+ json_path (str): The path of json
+ """
+
+ with open(json_path) as f:
+ try:
+ json_data = json.loads(f.read())
+ except Exception:
+ raise ValueError(f"Could not read JSON file: {json_path}")
+ return json_data
diff --git a/agenta-backend/agenta_backend/services/results_service.py b/agenta-backend/agenta_backend/services/results_service.py
index 8b794a68ea..28b2b0ec2b 100644
--- a/agenta-backend/agenta_backend/services/results_service.py
+++ b/agenta-backend/agenta_backend/services/results_service.py
@@ -1,110 +1,114 @@
-from agenta_backend.services.db_mongo import evaluation_scenarios
+from agenta_backend.utils.common import engine
+from agenta_backend.services.db_manager import query
+from agenta_backend.models.db_models import EvaluationScenarioDB, EvaluationDB
+from agenta_backend.services import evaluation_service
+from agenta_backend.services import db_manager
+from agenta_backend.models.api.evaluation_model import EvaluationType
from bson import ObjectId
-async def fetch_results_for_human_a_b_testing_evaluation(
- evaluation_id: str, variants: list
-):
- results = {}
- evaluation_rows_nb = await evaluation_scenarios.count_documents(
- {"evaluation_id": evaluation_id, "vote": {"$ne": ""}}
+async def fetch_results_for_evaluation(evaluation: EvaluationDB):
+ evaluation_scenarios = await engine.find(
+ EvaluationScenarioDB, EvaluationScenarioDB.evaluation == ObjectId(evaluation.id)
)
- if evaluation_rows_nb == 0:
+ results = {}
+ if len(evaluation_scenarios) == 0:
return results
- results["variants"] = variants
- results["variants_votes_data"] = {}
- results["nb_of_rows"] = evaluation_rows_nb
-
- flag_votes_nb = await evaluation_scenarios.count_documents(
- {"vote": "0", "evaluation_id": evaluation_id}
- )
- results["flag_votes"] = {}
- results["flag_votes"]["number_of_votes"] = flag_votes_nb
- results["flag_votes"]["percentage"] = (
- round(flag_votes_nb / evaluation_rows_nb * 100, 2) if evaluation_rows_nb else 0
- )
-
- for item in variants:
- results["variants_votes_data"][item] = {}
- variant_votes_nb: int = await evaluation_scenarios.count_documents(
- {"vote": item, "evaluation_id": evaluation_id}
+ results["variants"] = [str(variant) for variant in evaluation.variants]
+ variant_names = []
+ for variant_id in evaluation.variants:
+ variant = await db_manager.get_app_variant_instance_by_id(str(variant_id))
+ variant_name = variant.variant_name if variant else str(variant_id)
+ variant_names.append(str(variant_name))
+ results["variant_names"] = variant_names
+ results["nb_of_rows"] = len(evaluation_scenarios)
+ if evaluation.evaluation_type == EvaluationType.human_a_b_testing:
+ results.update(
+ await _compute_stats_for_human_a_b_testing_evaluation(evaluation_scenarios)
)
- results["variants_votes_data"][item]["number_of_votes"] = variant_votes_nb
- results["variants_votes_data"][item]["percentage"] = (
- round(variant_votes_nb / evaluation_rows_nb * 100, 2)
- if evaluation_rows_nb
- else 0
+ elif evaluation.evaluation_type == EvaluationType.auto_exact_match:
+ results.update(
+ await _compute_stats_for_evaluation(
+ evaluation_scenarios, classes=["correct", "wrong"]
+ )
+ )
+ elif evaluation.evaluation_type == EvaluationType.auto_similarity_match:
+ results.update(
+ await _compute_stats_for_evaluation(
+ evaluation_scenarios, classes=["true", "false"]
+ )
+ )
+ elif evaluation.evaluation_type == EvaluationType.auto_regex_test:
+ results.update(
+ await _compute_stats_for_evaluation(
+ evaluation_scenarios, classes=["correct", "wrong"]
+ )
)
return results
-async def fetch_results_for_auto_exact_match_evaluation(
- evaluation_id: str, variant: str
-):
+async def _compute_stats_for_evaluation(evaluation_scenarios: list, classes: list):
results = {}
- evaluation_rows_nb = await evaluation_scenarios.count_documents(
- {"evaluation_id": evaluation_id, "score": {"$ne": ""}}
- )
-
- if evaluation_rows_nb == 0:
- return results
-
- results["variant"] = variant
- # results["variants_scores_data"] = {}
- results["nb_of_rows"] = evaluation_rows_nb
-
- correct_scores_nb: int = await evaluation_scenarios.count_documents(
- {"score": "correct", "evaluation_id": evaluation_id}
- )
-
- wrong_scores_nb: int = await evaluation_scenarios.count_documents(
- {"score": "wrong", "evaluation_id": evaluation_id}
- )
- results["scores"] = {}
- results["scores"]["correct"] = correct_scores_nb
- results["scores"]["wrong"] = wrong_scores_nb
+ for cl in classes:
+ results[cl] = [
+ scenario for scenario in evaluation_scenarios if scenario.score == cl
+ ]
return results
-async def fetch_results_for_auto_similarity_match_evaluation(
- evaluation_id: str, variant: str
-):
+async def _compute_stats_for_human_a_b_testing_evaluation(evaluation_scenarios: list):
results = {}
- evaluation_rows_nb = await evaluation_scenarios.count_documents(
- {"evaluation_id": evaluation_id, "score": {"$ne": ""}}
- )
-
- if evaluation_rows_nb == 0:
- return results
-
- results["variant"] = variant
- results["nb_of_rows"] = evaluation_rows_nb
-
- similar_scores_nb: int = await evaluation_scenarios.count_documents(
- {"score": "true", "evaluation_id": evaluation_id}
- )
+ results["variants_votes_data"] = {}
+ results["flag_votes"] = {}
- dissimilar_scores_nb: int = await evaluation_scenarios.count_documents(
- {"score": "false", "evaluation_id": evaluation_id}
+ flag_votes_nb = [
+ scenario for scenario in evaluation_scenarios if scenario.vote == "0"
+ ]
+ results["flag_votes"]["number_of_votes"] = len(flag_votes_nb)
+ results["flag_votes"]["percentage"] = (
+ round(len(flag_votes_nb) / len(evaluation_scenarios) * 100, 2)
+ if len(evaluation_scenarios)
+ else 0
)
- results["scores"] = {}
- results["scores"]["true"] = similar_scores_nb
- results["scores"]["false"] = dissimilar_scores_nb
+ for scenario in evaluation_scenarios:
+ if scenario.vote not in results["variants_votes_data"]:
+ results["variants_votes_data"][scenario.vote] = {}
+ results["variants_votes_data"][scenario.vote]["number_of_votes"] = 1
+ else:
+ results["variants_votes_data"][scenario.vote]["number_of_votes"] += 1
+ for key, value in results["variants_votes_data"].items():
+ value["percentage"] = round(
+ value["number_of_votes"] / len(evaluation_scenarios) * 100, 2
+ )
return results
async def fetch_results_for_auto_ai_critique(evaluation_id: str):
pipeline = [
- {"$match": {"evaluation_id": evaluation_id}},
- {"$group": {"_id": "$evaluation", "count": {"$sum": 1}}},
+ {"$match": {"evaluations": ObjectId(evaluation_id)}},
+ {"$group": {"_id": "$score", "count": {"$sum": 1}}},
]
results = {}
- aggregation_cursor = evaluation_scenarios.aggregate(pipeline)
-
- async for doc in aggregation_cursor:
+ collection = engine.get_collection(EvaluationScenarioDB)
+ aggregation_cursor = await collection.aggregate(pipeline).to_list(length=None)
+ for doc in aggregation_cursor:
results[doc["_id"]] = doc["count"]
-
return results
+
+
+async def fetch_average_score_for_custom_code_run(evaluation_id: str) -> float:
+ query_exp = EvaluationScenarioDB.evaluation == ObjectId(evaluation_id)
+ eval_scenarios = await engine.find(EvaluationScenarioDB, query_exp)
+
+ list_of_scores = []
+ for scenario in eval_scenarios:
+ score = scenario.score
+ if not scenario.score:
+ score = 0
+ list_of_scores.append(round(float(score), 2))
+
+ average_score = sum(list_of_scores) / len(list_of_scores)
+ return average_score
diff --git a/agenta-backend/agenta_backend/services/security/__init__.py b/agenta-backend/agenta_backend/services/security/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/agenta-backend/agenta_backend/services/security/sandbox.py b/agenta-backend/agenta_backend/services/security/sandbox.py
new file mode 100644
index 0000000000..63974b8c01
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/security/sandbox.py
@@ -0,0 +1,97 @@
+from typing import Union, Text, Dict, Any
+
+from RestrictedPython import safe_builtins, compile_restricted
+from RestrictedPython.Eval import (
+ default_guarded_getiter,
+ default_guarded_getitem,
+)
+from RestrictedPython.Guards import (
+ guarded_iter_unpack_sequence,
+ full_write_guard,
+)
+
+
+def is_import_safe(python_code: Text) -> bool:
+ """Checks if the imports in the python code contains a system-level import.
+
+ Args:
+ python_code (str): The Python code to be executed
+
+ Returns:
+ bool - module is secured or not
+ """
+
+ disallowed_imports = ["os", "subprocess", "threading", "multiprocessing"]
+ for import_ in disallowed_imports:
+ if import_ in python_code:
+ return False
+ return True
+
+
+def execute_code_safely(
+ app_params: Dict[str, str],
+ inputs: Dict[str, str],
+ output: str,
+ correct_answer: str,
+ code: Text,
+) -> Union[float, None]:
+ """
+ Execute the provided Python code safely using RestrictedPython.
+
+ Args:
+ - app_params (Dict[str, str]): The parameters of the app variant.
+ - inputs (dict): Inputs to be used during code execution.
+ - output (str): The output of the app variant after being called.
+ - correct_answer (str): The correct answer (or target) of the app variant.
+ - code (Text): The Python code to be executed.
+
+ Returns:
+ - (float): Result of the execution if successful. Should be between 0 and 1.
+ - None if execution fails or result is not a float between 0 and 1.
+ """
+ # Define the available built-ins
+ local_builtins = safe_builtins.copy()
+
+ # Add the __import__ built-in function to the local builtins
+ local_builtins["__import__"] = __import__
+
+ # Define supported packages
+ allowed_imports = [
+ "math",
+ "random",
+ "datetime",
+ "json",
+ "jsonschema",
+ "requests",
+ "numpy",
+ ]
+
+ # Create a dictionary to simulate allowed imports
+ allowed_modules = {}
+ for package_name in allowed_imports:
+ allowed_modules[package_name] = __import__(package_name)
+
+ # Add the allowed modules to the local built-ins
+ local_builtins.update(allowed_modules)
+
+ # Define the environment for the code execution
+ environment = {
+ "_getiter_": default_guarded_getiter,
+ "_getitem_": default_guarded_getitem,
+ "_iter_unpack_sequence_": guarded_iter_unpack_sequence,
+ "_write_": full_write_guard,
+ "__builtins__": local_builtins,
+ }
+
+ # Compile the code in a restricted environment
+ byte_code = compile_restricted(code, filename="", mode="exec")
+
+ # Execute the code
+ exec(byte_code, environment)
+
+ # Call the evaluation function, extract the result if it exists
+ # and is a float between 0 and 1
+ result = environment["evaluate"](app_params, inputs, correct_answer, output)
+ if isinstance(result, float) and 0 <= result <= 1:
+ return result
+ return None
diff --git a/agenta-backend/agenta_backend/services/selectors.py b/agenta-backend/agenta_backend/services/selectors.py
new file mode 100644
index 0000000000..20c3e38b6f
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/selectors.py
@@ -0,0 +1,73 @@
+from bson import ObjectId
+from typing import Tuple, Dict, List
+from agenta_backend.models.db_models import (
+ UserDB,
+ OrganizationDB,
+)
+
+from odmantic import query
+from agenta_backend.utils.common import engine
+from agenta_backend.models.api.organization_models import Organization
+
+
+async def get_user_and_org_id(user_uid_id) -> Dict[str, List]:
+ """Retrieves the user ID and organization ID based on the logged-in session.
+
+ Arguments:
+ session (SessionContainer): Used to store and manage the user's session data
+
+ Returns:
+ A dictionary containing the user_id and a list of the user's organization_ids.
+ """
+ user_id, org_ids = await get_user_objectid(user_uid_id)
+ return {"uid": user_id, "organization_ids": org_ids}
+
+
+async def get_user_objectid(user_uid: str) -> Tuple[str, List]:
+ """Retrieves the user object ID and organization IDs from the database
+ based on the user ID.
+
+ Arguments:
+ user_id (str): The unique identifier of a user
+
+ Returns:
+ a tuple containing the string representation of the user's ObjectId and the List
+ of the user's organization_ids.
+ """
+
+ user = await engine.find_one(UserDB, UserDB.uid == user_uid)
+
+ if user is not None:
+ user_id = str(user.uid)
+ organization_ids: List = (
+ [org for org in user.organizations] if user.organizations else []
+ )
+ return user_id, organization_ids
+
+ return None, []
+
+
+async def get_user_own_org(user_uid: str) -> OrganizationDB:
+ """Get's the default users' organization from the database.
+
+ Arguments:
+ user_uid (str): The uid of the user
+
+ Returns:
+ Organization: Instance of OrganizationDB
+ """
+
+ user = await engine.find_one(UserDB, UserDB.uid == user_uid)
+
+ # Build the query expression for the two conditions
+ query_expression = query.eq(OrganizationDB.owner, str(user.id)) & query.eq(
+ OrganizationDB.type, "default"
+ )
+
+ # get the organization
+ org: OrganizationDB = await engine.find_one(OrganizationDB, query_expression)
+
+ if org is not None:
+ return org
+ else:
+ return None
diff --git a/agenta-backend/agenta_backend/services/templates_manager.py b/agenta-backend/agenta_backend/services/templates_manager.py
new file mode 100644
index 0000000000..28b793ce00
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/templates_manager.py
@@ -0,0 +1,181 @@
+import json
+import backoff
+from typing import Any, Dict, List
+import httpx
+import os
+from agenta_backend.config import settings
+from agenta_backend.services import db_manager
+from agenta_backend.utils import redis_utils
+from httpx import ConnectError, TimeoutException
+from asyncio.exceptions import CancelledError
+
+if os.environ["FEATURE_FLAG"] in ["oss", "cloud"]:
+ from agenta_backend.services import container_manager
+
+from typing import Union
+
+
+async def update_and_sync_templates(cache: bool = True) -> None:
+ """
+ Updates and synchronizes templates by retrieving templates from DockerHub and S3, adding new templates to the database,
+ and removing old templates from the database.
+
+ Args:
+ cache (bool): A boolean flag indicating whether to use cached templates or not. Defaults to True.
+
+ Returns:
+ None
+ """
+ templates = await retrieve_templates_from_dockerhub_cached(cache)
+
+ templates_ids_not_to_remove = []
+ templates_info = await retrieve_templates_info_from_s3(cache)
+ for temp in templates:
+ if temp["name"] in list(templates_info.keys()):
+ templates_ids_not_to_remove.append(int(temp["id"]))
+ temp_info = templates_info[temp["name"]]
+ template_id = await db_manager.add_template(
+ **{
+ "tag_id": int(temp["id"]),
+ "name": temp["name"],
+ "repo_name": temp.get("last_updater_username", "repo_name"),
+ "title": temp_info["name"],
+ "description": temp_info["description"],
+ "size": (
+ temp["images"][0]["size"]
+ if not temp.get("size", None)
+ else temp["size"]
+ ),
+ "digest": temp["digest"],
+ "last_pushed": (
+ temp["images"][0]["last_pushed"]
+ if not temp.get("last_pushed", None)
+ else temp["last_pushed"]
+ ),
+ }
+ )
+ print(f"Template {template_id} added to the database.")
+
+ # Get docker hub config
+ repo_owner = settings.docker_hub_repo_owner
+ repo_name = settings.docker_hub_repo_name
+
+ # Pull image from DockerHub
+ image_res = await container_manager.pull_docker_image(
+ repo_name=f"{repo_owner}/{repo_name}", tag=temp["name"]
+ )
+ print(f"Template Image {image_res[0]['id']} pulled from DockerHub.")
+
+ # Remove old templates from database
+ await db_manager.remove_old_template_from_db(templates_ids_not_to_remove)
+
+
+async def retrieve_templates_from_dockerhub_cached(cache: bool) -> List[dict]:
+ """Retrieves templates from Docker Hub and caches the data in Redis for future use.
+ Args:
+ cache: A boolean value that indicates whether to use the cached data or not.
+ Returns:
+ List of tags data (cached or network-call)
+ """
+ r = redis_utils.redis_connection()
+ if cache:
+ cached_data = r.get("templates_data")
+ if cached_data is not None:
+ return json.loads(cached_data.decode("utf-8"))
+
+ # If not cached, fetch data from Docker Hub and cache it in Redis
+ response = await retrieve_templates_from_dockerhub(
+ settings.docker_hub_url,
+ settings.docker_hub_repo_owner,
+ settings.docker_hub_repo_name,
+ )
+ response_data = response["results"]
+
+ # Cache the data in Redis for 60 minutes
+ r.set("templates_data", json.dumps(response_data), ex=900)
+ return response_data
+
+
+async def retrieve_templates_info_from_s3(
+ cache: bool,
+) -> Dict[str, Dict[str, Any]]:
+ """Retrieves templates information from s3 and caches the data in Redis for future use.
+
+ Args:
+ cache: A boolean value that indicates whether to use the cached data or not.
+
+ Returns:
+ Information about organization in s3 (cached or network-call)
+ """
+
+ r = redis_utils.redis_connection()
+ if cache:
+ cached_data = r.get("temp_data")
+ if cached_data is not None:
+ print("Using cache...")
+ return json.loads(cached_data)
+
+ # If not cached, fetch data from Docker Hub and cache it in Redis
+ response = await get_templates_info_from_s3(
+ "https://llm-app-json.s3.eu-central-1.amazonaws.com/llm_info.json"
+ )
+
+ # Cache the data in Redis for 60 minutes
+ r.set("temp_data", json.dumps(response), ex=900)
+ print("Using network call...")
+ return response
+
+
+@backoff.on_exception(backoff.expo, (ConnectError, CancelledError), max_tries=5)
+async def retrieve_templates_from_dockerhub(
+ url: str, repo_owner: str, repo_name: str
+) -> Union[List[dict], dict]:
+ """
+ Business logic to retrieve templates from DockerHub.
+
+ Args:
+ url (str): The URL endpoint for retrieving templates. Should contain placeholders `{}`
+ for the `repo_owner` and `repo_name` values to be inserted. For example:
+ `https://hub.docker.com/v2/repositories/{}/{}/tags`.
+ repo_owner (str): The owner or organization of the repository from which templates are to be retrieved.
+ repo_name (str): The name of the repository where the templates are located.
+
+ Returns:
+ tuple: A tuple containing two values.
+ """
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"{url.format(repo_owner, repo_name)}/tags", timeout=10
+ )
+ if response.status_code == 200:
+ response_data = response.json()
+ return response_data
+
+ response_data = response.json()
+ return response_data
+
+
+@backoff.on_exception(
+ backoff.expo, (ConnectError, TimeoutException, CancelledError), max_tries=5
+)
+async def get_templates_info_from_s3(url: str) -> Dict[str, Dict[str, Any]]:
+ """
+ Business logic to retrieve templates information from S3.
+
+ Args:
+ url (str): The URL endpoint for retrieving templates info.
+
+ Returns:
+ response_data (Dict[str, Dict[str, Any]]): A dictionary \
+ containing dictionaries of templates information.
+ """
+
+ async with httpx.AsyncClient() as client:
+ response = await client.get(url, timeout=10)
+ if response.status_code == 200:
+ response_data = response.json()
+ return response_data
+
+ response_data = response.json()
+ return response_data
diff --git a/agenta-backend/agenta_backend/services/user_service.py b/agenta-backend/agenta_backend/services/user_service.py
new file mode 100644
index 0000000000..1cf0dfc323
--- /dev/null
+++ b/agenta-backend/agenta_backend/services/user_service.py
@@ -0,0 +1,30 @@
+from agenta_backend.utils.common import engine
+from agenta_backend.models.db_models import UserDB
+from agenta_backend.models.api.user_models import User, UserUpdate
+
+
+async def create_new_user(payload: User) -> UserDB:
+ user_instance = UserDB(
+ uid=payload.uid,
+ username=payload.username,
+ email=payload.email,
+ )
+ user = await engine.save(user_instance)
+ return user
+
+
+async def update_user(user_uid: str, payload: UserUpdate) -> UserDB:
+ user = await engine.find_one(UserDB, UserDB.uid == user_uid)
+
+ if user is not None:
+ values_to_update = {key: value for key, value in payload.dict()}
+ updated_user = user.update(values_to_update)
+ await engine.save(updated_user)
+ return user
+ raise NotFound("Credentials not found. Please try again!")
+
+
+class NotFound(Exception):
+ """Custom exception for credentials not found"""
+
+ pass
diff --git a/agenta-backend/agenta_backend/tests/__init__.py b/agenta-backend/agenta_backend/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/agenta-backend/agenta_backend/tests/conftest.py b/agenta-backend/agenta_backend/tests/conftest.py
new file mode 100644
index 0000000000..0fb5fd275d
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/conftest.py
@@ -0,0 +1,20 @@
+import pytest
+import asyncio
+
+from agenta_backend.models.db_engine import DBEngine
+
+engine = DBEngine().engine()
+
+
+@pytest.fixture(scope="session", autouse=True)
+def event_loop():
+ """Create an instance of the default event loop for each test case."""
+ policy = asyncio.get_event_loop_policy()
+ res = policy.new_event_loop()
+ asyncio.set_event_loop(res)
+ res._close = res.close
+
+ yield res
+
+ res._close() # close event loop
+ DBEngine().remove_db() # drop database
diff --git a/agenta-backend/agenta_backend/tests/observability_router/conftest.py b/agenta-backend/agenta_backend/tests/observability_router/conftest.py
new file mode 100644
index 0000000000..5c4a38c8cd
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/observability_router/conftest.py
@@ -0,0 +1,127 @@
+import pytest
+from datetime import datetime
+
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.db_models import OrganizationDB
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+
+@pytest.fixture()
+def spans_db_data():
+ return [
+ {
+ "parent_span_id": "string",
+ "meta": {},
+ "event_name": "call",
+ "event_type": "fixture_call",
+ "start_time": str(datetime.utcnow()),
+ "duration": 8.30,
+ "status": "initiated",
+ "end_time": str(datetime.utcnow()),
+ "inputs": ["string"],
+ "outputs": ["string"],
+ "prompt_template": "string",
+ "tokens_input": 80,
+ "tokens_output": 25,
+ "token_total": 105,
+ "cost": 0.23,
+ "tags": ["string"],
+ },
+ {
+ "parent_span_id": "string",
+ "meta": {},
+ "event_name": "call",
+ "event_type": "fixture_call",
+ "start_time": str(datetime.utcnow()),
+ "duration": 13.30,
+ "status": "initiated",
+ "end_time": str(datetime.utcnow()),
+ "inputs": ["string"],
+ "outputs": ["string"],
+ "prompt_template": "string",
+ "tokens_input": 58,
+ "tokens_output": 65,
+ "token_total": 123,
+ "cost": 0.19,
+ "tags": ["string"],
+ },
+ {
+ "parent_span_id": "string",
+ "meta": {},
+ "event_name": "call",
+ "event_type": "fixture_call",
+ "start_time": str(datetime.utcnow()),
+ "duration": 18.30,
+ "status": "initiated",
+ "end_time": str(datetime.utcnow()),
+ "inputs": ["string"],
+ "outputs": ["string"],
+ "prompt_template": "string",
+ "tokens_input": 100,
+ "tokens_output": 35,
+ "token_total": 135,
+ "cost": 0.54,
+ "tags": ["string"],
+ },
+ ]
+
+
+@pytest.fixture()
+def image_create_data():
+ return {
+ "docker_id": "sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "tags": "agentaai/templates_v2:local_test_prompt",
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow(),
+ }
+
+
+@pytest.fixture()
+def app_variant_create_data():
+ return {
+ "variant_name": "v1",
+ "parameters": {},
+ "created_at": datetime.utcnow(),
+ "updated_at": datetime.utcnow(),
+ }
+
+
+@pytest.fixture()
+def trace_create_data():
+ return {
+ "cost": 0.782,
+ "latency": 20,
+ "status": "completed",
+ "token_consumption": 638,
+ "tags": ["string"],
+ "start_time": str(datetime.utcnow()),
+ "end_time": str(datetime.utcnow()),
+ }
+
+
+@pytest.fixture()
+def organization_create_data():
+ return {
+ "name": "Agenta",
+ "description": "Agenta is a platform for building and deploying machine learning models.",
+ }
+
+
+@pytest.fixture()
+def user_create_data():
+ return {
+ "uid": "0",
+ "username": "agenta",
+ "email": "demo@agenta.ai",
+ }
+
+
+@pytest.fixture()
+def feedbacks_create_data():
+ return [
+ {"feedback": "thumbs up", "score": 0, "meta": {}},
+ {"feedback": "thumbs down", "score": 10, "meta": {}},
+ ]
diff --git a/agenta-backend/agenta_backend/tests/observability_router/test_observability_router.py b/agenta-backend/agenta_backend/tests/observability_router/test_observability_router.py
new file mode 100644
index 0000000000..35dfb450fe
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/observability_router/test_observability_router.py
@@ -0,0 +1,290 @@
+import json
+import pytest
+import random
+from typing import List
+
+from agenta_backend.models.db_models import (
+ AppDB,
+ ConfigDB,
+ SpanDB,
+ UserDB,
+ TraceDB,
+ OrganizationDB,
+ ImageDB,
+ AppVariantDB,
+ VariantBaseDB,
+)
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.services import selectors
+
+import httpx
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+# Initialize http client
+test_client = httpx.AsyncClient()
+timeout = httpx.Timeout(timeout=5, read=None, write=5)
+
+# Set global variables
+BACKEND_API_HOST = "http://localhost:8001"
+
+
+@pytest.mark.asyncio
+async def test_create_spans_endpoint(spans_db_data):
+ response = await test_client.post(
+ f"{BACKEND_API_HOST}/observability/spans/",
+ json=spans_db_data[0],
+ timeout=timeout,
+ )
+ assert response.status_code == 200
+
+
+@pytest.mark.asyncio
+async def test_create_user_and_org(user_create_data, organization_create_data):
+ user_db = UserDB(**user_create_data)
+ await engine.save(user_db)
+
+ org_db = OrganizationDB(**organization_create_data, owner=str(user_db.id))
+ await engine.save(org_db)
+
+ user_db.organizations = [org_db.id]
+ await engine.save(user_db)
+
+ assert org_db.name == "Agenta"
+ assert user_db.username == "agenta"
+ assert user_db.organizations == [org_db.id]
+
+
+@pytest.mark.asyncio
+async def test_create_organization(organization_create_data):
+ user_db = await engine.find_one(UserDB, UserDB.uid == "0")
+ organization = OrganizationDB(
+ **organization_create_data,
+ type="default",
+ owner=str(user_db.id),
+ members=[user_db.id],
+ )
+ await engine.save(organization)
+
+
+@pytest.mark.asyncio
+async def test_create_image_in_db(image_create_data):
+ user_db = await engine.find_one(UserDB, UserDB.uid == "0")
+ organization_db = await engine.find_one(
+ OrganizationDB, OrganizationDB.owner == str(user_db.id)
+ )
+
+ image_db = ImageDB(**image_create_data, user=user_db, organization=organization_db)
+ await engine.save(image_db)
+
+ assert image_db.user.id == user_db.id
+ assert image_db.tags == image_create_data["tags"]
+
+
+@pytest.mark.asyncio
+async def test_create_appvariant_in_db(app_variant_create_data):
+ user_db = await engine.find_one(UserDB, UserDB.uid == "0")
+ organization_db = await selectors.get_user_own_org(user_db.uid)
+ image_db = await engine.find_one(ImageDB, ImageDB.user == user_db.id)
+
+ app = AppDB(
+ app_name="test_app",
+ organization=organization_db,
+ user=user_db,
+ )
+ await engine.save(app)
+
+ db_config = ConfigDB(
+ config_name="default",
+ parameters={},
+ )
+ await engine.save(db_config)
+
+ db_base = VariantBaseDB(
+ app=app,
+ organization=organization_db,
+ user=user_db,
+ base_name="app",
+ image=image_db,
+ )
+ await engine.save(db_base)
+
+ app_variant_db = AppVariantDB(
+ **app_variant_create_data,
+ app=app,
+ image=image_db,
+ user=user_db,
+ organization=organization_db,
+ base_name="app",
+ config_name="default",
+ base=db_base,
+ config=db_config,
+ )
+ await engine.save(app_variant_db)
+
+ assert app_variant_db.image.id == image_db.id
+ assert app_variant_db.user.id == user_db.id
+
+
+@pytest.mark.asyncio
+async def test_create_spans_in_db(spans_db_data):
+ # Set previous span id to None and
+ # first span id used to False
+ previous_span_id = None
+ first_span_id_used = False
+
+ # Remove first item in a list (because we are
+ # already using it in the first test)
+ spans_db_data.pop(0)
+
+ for span_data in spans_db_data:
+ # In this case, we are getting the first span id that was
+ # created in the first test and updating the previous_span_id with it
+ if previous_span_id is None and not first_span_id_used:
+ first_span = await engine.find_one(SpanDB)
+ previous_span_id = str(first_span.id)
+
+ # Create a new span instance
+ span_db = SpanDB(**span_data)
+
+ # Set the parent_span_id to the new span instance if it exists
+ if previous_span_id is not None:
+ span_db.parent_span_id = previous_span_id
+
+ # Save the span instance and set the first_span_id_used
+ # to True to avoid reusing it
+ await engine.save(span_db)
+ first_span_id_used = True
+
+ # Check if the previous span id exists and that first_span_id_used is True
+ # if so, set the previous_span_id to the span that was created
+ if previous_span_id is not None and first_span_id_used:
+ previous_span_id = str(span_db.id)
+
+ assert len(spans_db_data) == 2
+
+
+@pytest.mark.asyncio
+async def fetch_spans_id():
+ spans = await engine.find(SpanDB)
+ assert type(spans) == List[SpanDB]
+ assert len(spans) == 3
+
+
+@pytest.mark.asyncio
+async def test_create_trace_endpoint(trace_create_data):
+ spans = await engine.find(SpanDB)
+ variants = await engine.find(AppVariantDB)
+
+ # Prepare required data
+ spans_id = [str(span.id) for span in spans]
+ app_id, variant_id = variants[0].app.id, variants[0].id
+
+ # Update trace_create_data
+ payload = {
+ "app_id": str(app_id),
+ "variant_id": str(variant_id),
+ **trace_create_data,
+ "spans": spans_id,
+ }
+ response = await test_client.post(
+ f"{BACKEND_API_HOST}/observability/traces/",
+ json=payload,
+ )
+ assert response.status_code == 200
+
+
+@pytest.mark.asyncio
+async def test_get_traces_endpoint():
+ variants = await engine.find(AppVariantDB)
+ app_id, variant_id = variants[0].app.id, variants[0].id
+
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/observability/traces/{str(app_id)}/{str(variant_id)}/"
+ )
+ assert response.status_code == 200
+ assert len(response.json()) == 1
+
+
+@pytest.mark.asyncio
+async def test_get_trace_endpoint():
+ traces = await engine.find(TraceDB)
+
+ variants = await engine.find(AppVariantDB)
+ app_id, variant_id = variants[0].app.id, variants[0].id
+
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/observability/traces/{str(traces[0].id)}/"
+ )
+ assert response.status_code == 200
+ assert len(response.json()["spans"]) == 3
+ assert response.json()["app_id"] == str(app_id)
+ assert response.json()["variant_id"] == str(variant_id)
+
+
+@pytest.mark.asyncio
+async def test_update_trace_status_endpoint():
+ payload = {
+ "status": random.choice(["initiated", "completed", "stopped", "cancelled"])
+ }
+
+ traces = await engine.find(TraceDB)
+ response = await test_client.put(
+ f"{BACKEND_API_HOST}/observability/traces/{str(traces[0].id)}/",
+ json=payload,
+ )
+ assert response.status_code == 200
+ assert response.json() == True
+
+
+@pytest.mark.asyncio
+async def test_create_feedback_endpoint(feedbacks_create_data):
+ traces = await engine.find(TraceDB)
+ for feedback_data in feedbacks_create_data:
+ response = await test_client.post(
+ f"{BACKEND_API_HOST}/observability/feedbacks/{str(traces[0].id)}/",
+ json=feedback_data,
+ )
+ assert response.status_code == 200
+ assert type(response.json()) == str
+
+
+@pytest.mark.asyncio
+async def test_get_trace_feedbacks_endpoint():
+ traces = await engine.find(TraceDB)
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/observability/feedbacks/{str(traces[0].id)}/"
+ )
+ assert response.status_code == 200
+ assert len(response.json()) == 2
+
+
+@pytest.mark.asyncio
+async def test_get_feedback_endpoint():
+ traces = await engine.find(TraceDB)
+ feedback_id = traces[0].feedbacks[0].uid
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/observability/feedbacks/{str(traces[0].id)}/{feedback_id}/"
+ )
+ assert response.status_code == 200
+ assert response.json()["feedback_id"] == feedback_id
+
+
+@pytest.mark.asyncio
+async def test_update_feedback_endpoint():
+ traces = await engine.find(TraceDB)
+ feedbacks_ids = [feedback.uid for feedback in traces[0].feedbacks]
+
+ for feedback_id in feedbacks_ids:
+ feedback_data = {
+ "feedback": random.choice(["thumbs up", "thumbs down"]),
+ "score": random.choice([50, 30]),
+ }
+ response = await test_client.put(
+ f"{BACKEND_API_HOST}/observability/feedbacks/{str(traces[0].id)}/{feedback_id}/",
+ json=feedback_data,
+ )
+ assert response.status_code == 200
+ assert response.json()["feedback_id"] == feedback_id
diff --git a/agenta-backend/agenta_backend/tests/organization_router/test_organization_router.py b/agenta-backend/agenta_backend/tests/organization_router/test_organization_router.py
new file mode 100644
index 0000000000..db06d86840
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/organization_router/test_organization_router.py
@@ -0,0 +1,49 @@
+import pytest
+
+from agenta_backend.services import selectors
+from agenta_backend.models.db_models import UserDB
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.api.organization_models import OrganizationOutput
+
+import httpx
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+# Initialize http client
+test_client = httpx.AsyncClient()
+timeout = httpx.Timeout(timeout=5, read=None, write=5)
+
+# Set global variables
+BACKEND_API_HOST = "http://localhost:8001"
+
+
+@pytest.mark.asyncio
+async def test_list_organizations():
+ response = await test_client.get(f"{BACKEND_API_HOST}/organizations/")
+
+ assert response.status_code == 200
+ assert len(response.json()) == 1
+
+
+@pytest.mark.asyncio
+async def test_get_user_organization():
+ user = await engine.find_one(UserDB, UserDB.uid == "0")
+ user_org = await selectors.get_user_own_org(user.uid)
+
+ response = await test_client.get(f"{BACKEND_API_HOST}/organizations/own/")
+
+ assert response.status_code == 200
+ assert response.json() == OrganizationOutput(
+ id=str(user_org.id), name=user_org.name
+ )
+
+
+@pytest.mark.asyncio
+async def test_user_does_not_have_an_organization():
+ user = UserDB(uid="0123", username="john_doe", email="johndoe@email.com")
+ await engine.save(user)
+
+ user_org = await selectors.get_user_own_org(user.uid)
+ assert user_org == None
diff --git a/agenta-backend/agenta_backend/tests/setenv.py b/agenta-backend/agenta_backend/tests/setenv.py
new file mode 100644
index 0000000000..7da873b813
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/setenv.py
@@ -0,0 +1,19 @@
+import os
+import logging
+import configparser
+
+
+# Initialize logger
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+def setup_pytest_variables():
+ config = configparser.ConfigParser()
+ config.read("./agenta_backend/pytest.ini")
+
+ for section in config.sections():
+ for key, value in config[section].items():
+ logger.info("Setting Pytest environment variables:")
+ logger.info(f"KEY: {key.upper()}, VALUE: {value}")
+ os.environ[key.upper()] = value
diff --git a/agenta-backend/agenta_backend/tests/testset_router/test_testset_router.py b/agenta-backend/agenta_backend/tests/testset_router/test_testset_router.py
new file mode 100644
index 0000000000..894fc40f08
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/testset_router/test_testset_router.py
@@ -0,0 +1,126 @@
+import pytest
+from pathlib import Path
+
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.db_models import (
+ AppDB,
+ TestSetDB,
+)
+
+import httpx
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+# Initialize http client
+test_client = httpx.AsyncClient()
+timeout = httpx.Timeout(timeout=5, read=None, write=5)
+
+# Set global variables
+BACKEND_API_HOST = "http://localhost:8001"
+TESTSET_SUBMODULE_DIR = Path(__file__).parent
+
+
+# TODO: test_csv_upload_file
+# TODO: test_json_upload_file
+
+
+@pytest.mark.asyncio
+async def test_create_testset():
+ app = await engine.find_one(AppDB, AppDB.app_name == "test_app")
+
+ payload = {
+ "name": "create_testset_main",
+ "csvdata": [
+ {
+ "country": "Comoros",
+ "correct_answer": "The capital of Comoros is Moroni",
+ },
+ {
+ "country": "Kyrgyzstan",
+ "correct_answer": "The capital of Kyrgyzstan is Bishkek",
+ },
+ {
+ "country": "Azerbaijan",
+ "correct_answer": "The capital of Azerbaijan is Baku",
+ },
+ ],
+ }
+ response = await test_client.post(
+ f"{BACKEND_API_HOST}/testsets/{str(app.id)}/", json=payload
+ )
+
+ assert response.status_code == 200
+ assert response.json()["name"] == payload["name"]
+
+
+@pytest.mark.asyncio
+async def test_update_testset():
+ app = await engine.find_one(AppDB, AppDB.app_name == "test_app")
+ testset = await engine.find_one(TestSetDB, TestSetDB.app == app.id)
+
+ payload = {
+ "name": "update_testset",
+ "csvdata": [
+ {
+ "country": "Comoros",
+ "correct_answer": "The capital of Comoros is Moroni",
+ },
+ {
+ "country": "Kyrgyzstan",
+ "correct_answer": "The capital of Kyrgyzstan is Bishkek",
+ },
+ {
+ "country": "Azerbaijan",
+ "correct_answer": "The capital of Azerbaijan is Baku",
+ },
+ ],
+ }
+ response = await test_client.put(
+ f"{BACKEND_API_HOST}/testsets/{str(testset.id)}/", json=payload
+ )
+
+ assert response.status_code == 200
+ assert response.json()["_id"] == str(testset.id)
+ assert response.json()["status"] == "success"
+ assert response.json()["message"] == "testset updated successfully"
+
+
+@pytest.mark.asyncio
+async def test_get_testsets():
+ app = await engine.find_one(AppDB, AppDB.app_name == "test_app")
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/testsets/?app_id={str(app.id)}"
+ )
+
+ assert response.status_code == 200
+ assert len(response.json()) == 1
+
+
+@pytest.mark.asyncio()
+async def test_get_testset():
+ app = await engine.find_one(AppDB, AppDB.app_name == "test_app")
+ testset = await engine.find_one(TestSetDB, TestSetDB.app == app.id)
+
+ response = await test_client.get(f"{BACKEND_API_HOST}/testsets/{str(testset.id)}/")
+
+ assert response.status_code == 200
+ assert response.json()["name"] == testset.name
+ assert response.json()["id"] == str(testset.id)
+
+
+@pytest.mark.asyncio
+async def test_delete_testsets():
+ app = await engine.find_one(AppDB, AppDB.app_name == "test_app")
+ testsets = await engine.find(TestSetDB, TestSetDB.app == app.id)
+
+ testset_ids = [str(testset.id) for testset in testsets]
+ payload = {"testset_ids": testset_ids}
+
+ response = await test_client.request(
+ method="DELETE", url=f"{BACKEND_API_HOST}/testsets/", json=payload
+ )
+
+ assert response.status_code == 200
+ assert response.json() == testset_ids
diff --git a/agenta-backend/agenta_backend/tests/user_profile_router/test_user_profile.py b/agenta-backend/agenta_backend/tests/user_profile_router/test_user_profile.py
new file mode 100644
index 0000000000..6d7912aaf3
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/user_profile_router/test_user_profile.py
@@ -0,0 +1,34 @@
+import pytest
+
+from agenta_backend.models.db_models import UserDB
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.api.user_models import User
+
+import httpx
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+# Initialize http client
+test_client = httpx.AsyncClient()
+timeout = httpx.Timeout(timeout=5, read=None, write=5)
+
+# Set global variables
+BACKEND_API_HOST = "http://localhost:8001"
+
+
+@pytest.mark.asyncio
+async def test_user_profile():
+ user_db = await engine.find_one(UserDB, UserDB.uid == "0")
+ user_db_dict = User(
+ id=str(user_db.id),
+ uid=str(user_db.uid),
+ username=str(user_db.username),
+ email=str(user_db.email),
+ ).dict(exclude_unset=True)
+
+ response = await test_client.get(f"{BACKEND_API_HOST}/profile/")
+
+ assert response.status_code == 200
+ assert response.json() == user_db_dict
diff --git a/agenta-backend/agenta_backend/tests/variants_router/conftest.py b/agenta-backend/agenta_backend/tests/variants_router/conftest.py
new file mode 100644
index 0000000000..f7c8d817aa
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/variants_router/conftest.py
@@ -0,0 +1,127 @@
+import pytest
+import logging
+
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.db_models import (
+ AppDB,
+ UserDB,
+ VariantBaseDB,
+ ImageDB,
+ ConfigDB,
+ AppVariantDB,
+ OrganizationDB,
+)
+
+from agenta_backend.services import selectors
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+@pytest.fixture(scope="function")
+async def get_first_user_object():
+ """Get the user object from the database or create a new one if not found."""
+
+ try:
+ user = await engine.find_one(UserDB, UserDB.uid == "0")
+ if user is None:
+ create_user = UserDB(uid="0")
+ await engine.save(create_user)
+
+ org = OrganizationDB(type="default", owner=str(create_user.id))
+ await engine.save(org)
+
+ create_user.organizations.append(org.id)
+ await engine.save(create_user)
+ await engine.save(org)
+
+ return create_user
+ else:
+ return user
+ except Exception as e:
+ pytest.fail(f"Failed to get or create the first user: {e}")
+
+
+@pytest.fixture(scope="function")
+async def get_second_user_object():
+ """Create a second user object."""
+
+ try:
+ user = await engine.find_one(UserDB, UserDB.uid == "1")
+ if user is None:
+ create_user = UserDB(
+ uid="1", username="test_user1", email="test_user1@email.com"
+ )
+ await engine.save(create_user)
+
+ org = OrganizationDB(type="default", owner=str(create_user.id))
+ await engine.save(org)
+
+ create_user.organizations.append(org.id)
+ await engine.save(create_user)
+ await engine.save(org)
+
+ return create_user
+ else:
+ return user
+
+ except Exception as e:
+ pytest.fail(f"Failed to get or create the second user: {e}")
+
+
+@pytest.fixture()
+async def get_first_user_app():
+ user = await engine.find_one(UserDB, UserDB.uid == "0")
+ if user is None:
+ user = UserDB(uid="0")
+ await engine.save(user)
+
+ organization = OrganizationDB(type="default", owner=str(user.id))
+ await engine.save(organization)
+
+ user.organizations.append(organization.id)
+ await engine.save(user)
+ await engine.save(organization)
+
+ organization = await selectors.get_user_own_org(user.uid)
+
+ app = AppDB(app_name="myapp", organization=organization, user=user)
+ await engine.save(app)
+
+ db_image = ImageDB(
+ docker_id="sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ tags="agentaai/templates_v2:local_test_prompt",
+ user=user,
+ organization=organization,
+ )
+ await engine.save(db_image)
+
+ db_config = ConfigDB(
+ config_name="default",
+ parameters={},
+ )
+ await engine.save(db_config)
+
+ db_base = VariantBaseDB(
+ base_name="app", image=db_image, organization=organization, user=user, app=app
+ )
+ await engine.save(db_base)
+
+ appvariant = AppVariantDB(
+ app=app,
+ variant_name="app",
+ image=db_image,
+ user=user,
+ organization=organization,
+ parameters={},
+ base_name="app",
+ config_name="default",
+ base=db_base,
+ config=db_config,
+ )
+ await engine.save(appvariant)
+
+ return appvariant, user, organization, app, db_image, db_config, db_base
diff --git a/agenta-backend/agenta_backend/tests/variants_router/test_app_variant_router.py b/agenta-backend/agenta_backend/tests/variants_router/test_app_variant_router.py
new file mode 100644
index 0000000000..7ece91b624
--- /dev/null
+++ b/agenta-backend/agenta_backend/tests/variants_router/test_app_variant_router.py
@@ -0,0 +1,167 @@
+import httpx
+import pytest
+import logging
+from bson import ObjectId
+
+from agenta_backend.routers import app_router
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.services import selectors, db_manager
+from agenta_backend.models.db_models import (
+ AppDB,
+ VariantBaseDB,
+ ImageDB,
+ ConfigDB,
+ AppVariantDB,
+)
+
+
+# Initialize database engine
+engine = DBEngine().engine()
+
+# Initialize http client
+test_client = httpx.AsyncClient()
+timeout = httpx.Timeout(timeout=5, read=None, write=5)
+
+# Generate a new ObjectId
+new_object_id = ObjectId()
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+# Set global variables
+BACKEND_API_HOST = "http://localhost:8001"
+
+
+@pytest.mark.asyncio
+async def test_create_app(get_first_user_object):
+ user = await get_first_user_object
+ organization = await selectors.get_user_own_org(user.uid)
+
+ response = await test_client.post(
+ f"{BACKEND_API_HOST}/apps/",
+ json={
+ "app_name": "app_variant_test",
+ "organization_id": str(organization.id),
+ },
+ timeout=timeout,
+ )
+ assert response.status_code == 200
+ assert response.json()["app_name"] == "app_variant_test"
+
+
+@pytest.mark.asyncio
+async def test_list_apps():
+ response = await test_client.get(f"{BACKEND_API_HOST}/apps/")
+
+ assert response.status_code == 200
+ assert len(response.json()) == 2
+
+
+@pytest.mark.asyncio
+async def test_create_app_variant(get_first_user_object):
+ user = await get_first_user_object
+ organization = await selectors.get_user_own_org(user.uid)
+ app = await engine.find_one(AppDB, AppDB.app_name == "app_variant_test")
+
+ db_image = ImageDB(
+ docker_id="sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ tags="agentaai/templates_v2:local_test_prompt",
+ user=user,
+ organization=organization,
+ )
+ await engine.save(db_image)
+
+ db_config = ConfigDB(
+ config_name="default",
+ parameters={},
+ )
+ await engine.save(db_config)
+
+ db_base = VariantBaseDB(
+ base_name="app",
+ app=app,
+ organization=organization,
+ user=user,
+ image=db_image,
+ )
+ await engine.save(db_base)
+
+ appvariant = AppVariantDB(
+ app=app,
+ variant_name="app",
+ image=db_image,
+ user=user,
+ organization=organization,
+ parameters={},
+ base_name="app",
+ config_name="default",
+ base=db_base,
+ config=db_config,
+ )
+ await engine.save(appvariant)
+
+ response = await test_client.get(f"{BACKEND_API_HOST}/apps/{str(app.id)}/variants/")
+ assert response.status_code == 200
+ assert len(response.json()) == 1
+
+
+@pytest.mark.asyncio
+async def test_list_app_variants():
+ app_db = await engine.find_one(AppDB, AppDB.app_name == "app_variant_test")
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/apps/{str(app_db.id)}/variants/"
+ )
+
+ assert response.status_code == 200
+ assert len(response.json()) == 1
+
+
+@pytest.mark.asyncio
+async def test_delete_app_without_permission(get_second_user_object):
+ user2 = await get_second_user_object
+ user2_organization = await selectors.get_user_own_org(user2.uid)
+
+ user2_app = AppDB(
+ app_name="test_app_by_user2",
+ organization=user2_organization,
+ user=user2,
+ )
+ await engine.save(user2_app)
+
+ response = await test_client.delete(
+ f"{BACKEND_API_HOST}/apps/{str(user2_app.id)}/",
+ timeout=timeout,
+ )
+ assert response.status_code == 400
+
+
+@pytest.mark.asyncio
+async def test_list_environments():
+ app = await engine.find_one(AppDB, AppDB.app_name == "app_variant_test")
+ response = await test_client.get(
+ f"{BACKEND_API_HOST}/apps/{str(app.id)}/environments/"
+ )
+
+ assert response.status_code == 200
+ assert len(response.json()) == 3
+
+
+@pytest.mark.asyncio
+async def test_get_variant_by_env(get_first_user_app):
+ _, _, _, app, _, _, _ = await get_first_user_app
+ environments = await db_manager.list_environments(app_id=str(app.id))
+
+ for environment in environments:
+ response = await app_router.get_variant_by_env(
+ app_id=str(app.id), environment=environment.name
+ )
+ assert response == []
+
+
+@pytest.mark.asyncio
+async def test_remove_app():
+ app = await engine.find_one(AppDB, AppDB.app_name == "app_variant_test")
+ await engine.delete(app)
+
+ app = await engine.find_one(AppDB, AppDB.app_name == "app_variant_test")
+ assert app == None
diff --git a/agenta-backend/agenta_backend/utils/__init__.py b/agenta-backend/agenta_backend/utils/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/agenta-backend/agenta_backend/utils/common.py b/agenta-backend/agenta_backend/utils/common.py
new file mode 100644
index 0000000000..57aad1ec95
--- /dev/null
+++ b/agenta-backend/agenta_backend/utils/common.py
@@ -0,0 +1,135 @@
+from bson import ObjectId
+from odmantic import query
+from fastapi.responses import JSONResponse
+from typing import Dict, List, Union, Optional
+from agenta_backend.models.db_engine import DBEngine
+from agenta_backend.models.db_models import (
+ UserDB,
+ AppVariantDB,
+ OrganizationDB,
+ AppDB,
+ VariantBaseDB,
+)
+import logging
+
+engine = DBEngine().engine()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+async def get_organization(org_id: str) -> OrganizationDB:
+ org = await engine.find_one(OrganizationDB, OrganizationDB.id == ObjectId(org_id))
+ if org is not None:
+ return org
+ else:
+ return None
+
+
+async def get_app_instance(
+ app_id: str, variant_name: str = None, show_deleted: bool = False
+) -> AppVariantDB:
+ if variant_name is not None:
+ query_expression = (
+ query.eq(AppVariantDB.is_deleted, show_deleted)
+ & query.eq(AppVariantDB.app, ObjectId(app_id))
+ & query.eq(AppVariantDB.variant_name, variant_name)
+ )
+ else:
+ query_expression = query.eq(AppVariantDB.is_deleted, show_deleted) & query.eq(
+ AppVariantDB.app_name, ObjectId(app_id)
+ )
+
+ print("query_expression: " + str(query_expression))
+
+ app_instance = await engine.find_one(AppVariantDB, query_expression)
+
+ print("app_instance: " + str(app_instance))
+ return app_instance
+
+
+async def check_user_org_access(
+ kwargs: dict, organization_id: str, check_owner=False
+) -> bool:
+ if check_owner: # Check that the user is the owner of the organization
+ user = await engine.find_one(UserDB, UserDB.uid == kwargs["uid"])
+ organization = await get_organization(organization_id)
+ if not organization:
+ logger.error("Organization not found")
+ raise Exception("Organization not found")
+ return organization.owner == str(user.id)
+ else:
+ user_organizations: List = kwargs["organization_ids"]
+ object_organization_id = ObjectId(organization_id)
+ logger.debug(
+ f"object_organization_id: {object_organization_id}, user_organizations: {user_organizations}"
+ )
+ return object_organization_id in user_organizations
+
+
+async def check_access_to_app(
+ user_org_data: Dict[str, Union[str, list]],
+ app: Optional[AppDB] = None,
+ app_id: Optional[str] = None,
+ check_owner: bool = False,
+) -> bool:
+ """
+ Check if a user has access to a specific application.
+
+ Args:
+ user_org_data (Dict[str, Union[str, list]]): User-specific information.
+ app (Optional[AppDB]): An instance of the AppDB model representing the application.
+ app_id (Optional[str]): The ID of the application.
+ check_owner (bool): Whether to check if the user is the owner of the application.
+
+ Returns:
+ bool: True if the user has access, False otherwise.
+
+ Raises:
+ Exception: If neither or both `app` and `app_id` are provided.
+
+ """
+ if (app is None) == (app_id is None):
+ raise Exception("Provide either app or app_id, not both or neither")
+
+ # Fetch the app if only app_id is provided.
+ if app is None:
+ app = await engine.find_one(AppDB, AppDB.id == ObjectId(app_id))
+ if app is None:
+ logger.error("App not found")
+ return False
+
+ # Check user's access to the organization linked to the app.
+ organization_id = app.organization.id
+ return await check_user_org_access(user_org_data, str(organization_id), check_owner)
+
+
+async def check_access_to_variant(
+ user_org_data: Dict[str, Union[str, list]],
+ variant_id: str,
+ check_owner: bool = False,
+) -> bool:
+ if variant_id is None:
+ raise Exception("No variant_id provided")
+ variant = await engine.find_one(
+ AppVariantDB, AppVariantDB.id == ObjectId(variant_id)
+ )
+ if variant is None:
+ logger.error("Variant not found")
+ return False
+ organization_id = variant.organization.id
+ return await check_user_org_access(user_org_data, str(organization_id), check_owner)
+
+
+async def check_access_to_base(
+ user_org_data: Dict[str, Union[str, list]],
+ base_id: str,
+ check_owner: bool = False,
+) -> bool:
+ if base_id is None:
+ raise Exception("No base_id provided")
+ base = await engine.find_one(VariantBaseDB, VariantBaseDB.id == ObjectId(base_id))
+ if base is None:
+ logger.error("Base not found")
+ return False
+ organization_id = base.organization.id
+ return await check_user_org_access(user_org_data, str(organization_id), check_owner)
diff --git a/agenta-backend/agenta_backend/utils/redis_utils.py b/agenta-backend/agenta_backend/utils/redis_utils.py
new file mode 100644
index 0000000000..3868a68a9b
--- /dev/null
+++ b/agenta-backend/agenta_backend/utils/redis_utils.py
@@ -0,0 +1,21 @@
+import os
+import redis
+from redis.exceptions import ConnectionError
+
+
+def redis_connection() -> redis.Redis:
+ """Returns a client object for connecting to a Redis service specified \
+ by the REDIS_URL environment variable.
+
+ :return: a Redis client object.
+ """
+
+ try:
+ redis_client = redis.from_url(url=os.environ.get("REDIS_URL", None))
+ except ConnectionRefusedError:
+ raise ConnectionRefusedError(
+ "Refuse connecting to redis service. Kindly check redis url."
+ )
+ except ConnectionError:
+ raise ConnectionError("Could not connect to redis service.")
+ return redis_client
diff --git a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_001_dataset_to_testset.py b/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_001_dataset_to_testset.py
deleted file mode 100644
index 2963e2824c..0000000000
--- a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_001_dataset_to_testset.py
+++ /dev/null
@@ -1,49 +0,0 @@
-""" The provided Python script uses pymongo to connect to a MongoDB database, "agenta".
- It retrieves all documents from a source collection, "datasets", processes these
- documents in chunks to manage memory efficiently, and inserts the documents into a
- target collection, "testsets", if they do not already exist there, preventing duplication.
-"""
-
-
-from pymongo import MongoClient
-
-# Initialize the MongoDB client
-client = MongoClient("mongodb://username:password@0.0.0.0:27017")
-
-# Specify the database
-db = client["agenta"]
-
-# Specify the source and target collections
-source_collection = db["datasets"]
-target_collection = db["testsets"]
-
-# Fetch all documents from the source collection
-source_documents = source_collection.find()
-# print(source_documents.count())
-
-# Calculate Total Documents
-total_docs = source_collection.count_documents({})
-
-# Define Chunk Size
-chunk_size = 100
-
-# Calculate Number of Chunks
-num_chunks = total_docs // chunk_size + (total_docs % chunk_size > 0)
-
-# Fetch and Process Chunks
-for i in range(num_chunks):
- # Define Skip and Limit Values
- skip = i * chunk_size
- limit = chunk_size
-
- # Fetch Chunk of Documents
- chunk_docs = source_collection.find({}).skip(skip).limit(limit)
-
- # Process Documents
- for doc in chunk_docs:
- # Check if this document is already in the target collection
- already_exists = target_collection.find_one(doc["_id"])
-
- # If the document does not exist in the target collection, insert it
- if not already_exists:
- target_collection.insert_one(doc)
diff --git a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_002_comparison_table_dataset_field_to_testcase.py b/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_002_comparison_table_dataset_field_to_testcase.py
deleted file mode 100644
index 442a52a78f..0000000000
--- a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_002_comparison_table_dataset_field_to_testcase.py
+++ /dev/null
@@ -1,46 +0,0 @@
-""" The provided Python script uses pymongo to connect to a MongoDB database, "agenta".
- It retrieves all documents from a source collection, "datasets", processes these
- documents in chunks to manage memory efficiently, and inserts the documents into a
- target collection, "testsets", if they do not already exist there, preventing duplication.
-"""
-from pymongo import MongoClient
-
-# Initialize the MongoDB client
-client = MongoClient("mongodb://username:password@0.0.0.0:27017")
-
-# Specify the database
-db = client["agenta"]
-
-# Specify the source and target collections
-source_collection = db["evaluations"]
-
-# Fetch all documents from the source collection
-source_documents = source_collection.find()
-# print(source_documents.count())
-
-# Calculate Total Documents
-total_docs = source_collection.count_documents({})
-
-# Define Chunk Size
-chunk_size = 100
-
-# Calculate Number of Chunks
-num_chunks = total_docs // chunk_size + (total_docs % chunk_size > 0)
-
-# Fetch and Process Chunks
-for i in range(num_chunks):
- # Define Skip and Limit Values
- skip = i * chunk_size
- limit = chunk_size
-
- # Fetch Chunk of Documents
- chunk_docs = source_collection.find({}).skip(skip).limit(limit)
- # iterate over each document
- for doc in chunk_docs:
- # change the key from 'dataset' to 'testset'
- if "dataset" in doc:
- # generate new document with changed key and same value
- new_doc = {"testset" if k == "dataset" else k: v for k, v in doc.items()}
-
- # replace the old document with the new one
- source_collection.find_one_and_replace(doc, new_doc)
diff --git a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_003_comparisonTable_to_evaluation.py b/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_003_comparisonTable_to_evaluation.py
deleted file mode 100644
index f299204eac..0000000000
--- a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/m_003_comparisonTable_to_evaluation.py
+++ /dev/null
@@ -1,49 +0,0 @@
-""" The provided Python script uses pymongo to connect to a MongoDB database, "agenta".
- It retrieves all documents from a source collection, "datasets", processes these
- documents in chunks to manage memory efficiently, and inserts the documents into a
- target collection, "testsets", if they do not already exist there, preventing duplication.
-"""
-
-
-from pymongo import MongoClient
-
-# Initialize the MongoDB client
-client = MongoClient("mongodb://username:password@0.0.0.0:27017")
-
-# Specify the database
-db = client["agenta"]
-
-# Specify the source and target collections
-source_collection = db["comparison_tables"]
-target_collection = db["evaluation"]
-
-# Fetch all documents from the source collection
-source_documents = source_collection.find()
-# print(source_documents.count())
-
-# Calculate Total Documents
-total_docs = source_collection.count_documents({})
-
-# Define Chunk Size
-chunk_size = 100
-
-# Calculate Number of Chunks
-num_chunks = total_docs // chunk_size + (total_docs % chunk_size > 0)
-
-# Fetch and Process Chunks
-for i in range(num_chunks):
- # Define Skip and Limit Values
- skip = i * chunk_size
- limit = chunk_size
-
- # Fetch Chunk of Documents
- chunk_docs = source_collection.find({}).skip(skip).limit(limit)
-
- # Process Documents
- for doc in chunk_docs:
- # Check if this document is already in the target collection
- already_exists = target_collection.find_one(doc["_id"])
-
- # If the document does not exist in the target collection, insert it
- if not already_exists:
- target_collection.insert_one(doc)
diff --git a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/run_migrations.py b/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/run_migrations.py
deleted file mode 100644
index cf7f8cc9a5..0000000000
--- a/agenta-backend/db/migration/mongodb/v0.1.20->v0.1.21/run_migrations.py
+++ /dev/null
@@ -1,3 +0,0 @@
-import m_001_dataset_to_testset
-import m_002_comparison_table_dataset_field_to_testcase
-import m_003_comparisonTable_to_evaluation
diff --git a/agenta-backend/poetry.lock b/agenta-backend/poetry.lock
index edcd56cdd6..3b392c3054 100644
--- a/agenta-backend/poetry.lock
+++ b/agenta-backend/poetry.lock
@@ -1,9 +1,10 @@
-# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "aiodocker"
version = "0.21.0"
description = "Docker API client for asyncio"
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -17,98 +18,99 @@ typing-extensions = ">=3.6.5"
[[package]]
name = "aiohttp"
-version = "3.8.5"
+version = "3.8.6"
description = "Async http client/server framework (asyncio)"
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"},
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"},
- {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"},
- {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"},
- {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"},
- {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"},
- {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"},
- {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"},
- {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"},
- {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"},
- {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"},
- {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"},
- {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"},
- {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"},
- {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"},
- {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"},
- {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"},
- {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"},
- {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"},
- {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"},
- {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"},
- {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"},
- {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"},
- {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"},
- {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"},
- {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"},
- {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"},
- {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"},
- {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"},
- {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"},
- {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"},
+ {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"},
+ {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"},
+ {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"},
+ {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"},
+ {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"},
+ {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"},
+ {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"},
+ {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"},
+ {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"},
+ {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"},
+ {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"},
+ {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"},
+ {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"},
+ {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"},
+ {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"},
+ {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"},
+ {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"},
+ {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"},
+ {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"},
+ {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"},
+ {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"},
+ {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"},
+ {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"},
+ {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"},
+ {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"},
+ {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"},
+ {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"},
+ {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"},
+ {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"},
+ {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"},
+ {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"},
+ {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"},
+ {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"},
+ {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"},
+ {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"},
+ {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"},
+ {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"},
+ {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"},
+ {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"},
+ {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"},
+ {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"},
+ {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"},
+ {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"},
+ {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"},
+ {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"},
+ {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"},
+ {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"},
+ {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"},
+ {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"},
+ {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"},
+ {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"},
+ {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"},
+ {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"},
+ {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"},
+ {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"},
+ {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"},
+ {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"},
+ {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"},
+ {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"},
+ {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"},
+ {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"},
+ {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"},
+ {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"},
]
[package.dependencies]
@@ -127,6 +129,7 @@ speedups = ["Brotli", "aiodns", "cchardet"]
name = "aiosignal"
version = "1.3.1"
description = "aiosignal: a list of registered asynchronous callbacks"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -137,10 +140,27 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
+[[package]]
+name = "aiosmtplib"
+version = "1.1.6"
+description = "asyncio SMTP client"
+category = "main"
+optional = false
+python-versions = ">=3.5.2,<4.0.0"
+files = [
+ {file = "aiosmtplib-1.1.6-py3-none-any.whl", hash = "sha256:84174765778b2c5e0e207fbce0a769202fcf0c3de81faa87cc03551a6333bfa9"},
+ {file = "aiosmtplib-1.1.6.tar.gz", hash = "sha256:d138fe6ffecbc9e6320269690b9ac0b75e540ef96e8f5c77d4a306760014dce2"},
+]
+
+[package.extras]
+docs = ["sphinx (>=2,<4)", "sphinx_autodoc_typehints (>=1.7.0,<2.0.0)"]
+uvloop = ["uvloop (>=0.13,<0.15)"]
+
[[package]]
name = "anyio"
version = "3.7.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -158,10 +178,29 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (<0.22)"]
+[[package]]
+name = "asgiref"
+version = "3.7.2"
+description = "ASGI specs, helper code, and adapters"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
+ {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
+
+[package.extras]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
+
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -169,10 +208,26 @@ files = [
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
+[[package]]
+name = "asyncer"
+version = "0.0.2"
+description = "Asyncer, async and await, focused on developer experience."
+category = "main"
+optional = false
+python-versions = ">=3.6.2,<4.0.0"
+files = [
+ {file = "asyncer-0.0.2-py3-none-any.whl", hash = "sha256:46e0e1423ce21588350ad425875e81795280b9e1f517e8a389de940b86c348bd"},
+ {file = "asyncer-0.0.2.tar.gz", hash = "sha256:d546c85f3626ebbaf06bb4395db49761c902a61a6ac802b1a74133cab4f7f433"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<4.0.0"
+
[[package]]
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -191,6 +246,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "backoff"
version = "2.2.1"
description = "Function decoration for backoff and retry"
+category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
@@ -198,10 +254,54 @@ files = [
{file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
]
+[[package]]
+name = "boto3"
+version = "1.28.63"
+description = "The AWS SDK for Python"
+category = "main"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "boto3-1.28.63-py3-none-any.whl", hash = "sha256:65d052ec13197460586ee385aa2d6bba0e7378d2d2c7f3e93c044c43ae1ca782"},
+ {file = "boto3-1.28.63.tar.gz", hash = "sha256:94218aba2feb5b404b665b8d76c172dc654f79b4c5fa0e9e92459c098da87bf4"},
+]
+
+[package.dependencies]
+botocore = ">=1.31.63,<1.32.0"
+jmespath = ">=0.7.1,<2.0.0"
+s3transfer = ">=0.7.0,<0.8.0"
+
+[package.extras]
+crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
+
+[[package]]
+name = "botocore"
+version = "1.31.67"
+description = "Low-level, data-driven core of boto 3."
+category = "main"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "botocore-1.31.67-py3-none-any.whl", hash = "sha256:487fb6ee4a6612613da370599b1a1aca0e159dd9e94b2e8aaa8e6ad9cc546ded"},
+ {file = "botocore-1.31.67.tar.gz", hash = "sha256:ab3b73a2e03efa1c534a94f8db4a5cf45629a53e5478d2d154b0a3e2ffb05249"},
+]
+
+[package.dependencies]
+jmespath = ">=0.7.1,<2.0.0"
+python-dateutil = ">=2.1,<3.0.0"
+urllib3 = [
+ {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""},
+ {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""},
+]
+
+[package.extras]
+crt = ["awscrt (==0.16.26)"]
+
[[package]]
name = "certifi"
version = "2023.7.22"
description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -209,99 +309,181 @@ files = [
{file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
]
+[[package]]
+name = "cffi"
+version = "1.16.0"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
+ {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
+ {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
+ {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
+ {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
+ {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
+ {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
+ {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
+ {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
+ {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
+ {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
+ {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
+ {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
+ {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
+ {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
+ {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
+ {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
+ {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
+ {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
+ {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
+ {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
+ {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
+ {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
+ {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
+ {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
+ {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
+ {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
+ {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
+ {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
+ {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
+ {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
+ {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
[[package]]
name = "charset-normalizer"
-version = "3.2.0"
+version = "3.3.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
- {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
- {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
- {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
- {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
- {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
- {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
- {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
+ {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"},
+ {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"},
+ {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"},
+ {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"},
+ {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"},
+ {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"},
+ {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"},
+ {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"},
]
[[package]]
name = "click"
-version = "8.1.6"
+version = "8.1.7"
description = "Composable command line interface toolkit"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
- {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
@@ -311,6 +493,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
+category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -318,10 +501,57 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+[[package]]
+name = "cryptography"
+version = "41.0.4"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"},
+ {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"},
+ {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"},
+ {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"},
+ {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"},
+ {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"},
+ {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"},
+ {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"},
+ {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"},
+ {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"},
+ {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"},
+]
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
+docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
+nox = ["nox"]
+pep8test = ["black", "check-sdist", "mypy", "ruff"]
+sdist = ["build"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
+test-randomorder = ["pytest-randomly"]
+
[[package]]
name = "dataclasses-json"
version = "0.5.9"
description = "Easily serialize dataclasses to and from JSON"
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -337,10 +567,29 @@ typing-inspect = ">=0.4.0"
[package.extras]
dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest (>=7.2.0)", "setuptools", "simplejson", "twine", "types-dataclasses", "wheel"]
+[[package]]
+name = "deprecated"
+version = "1.2.13"
+description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
+ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
+]
+
+[package.dependencies]
+wrapt = ">=1.10,<2"
+
+[package.extras]
+dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"]
+
[[package]]
name = "dnspython"
version = "2.4.2"
description = "DNS toolkit"
+category = "main"
optional = false
python-versions = ">=3.8,<4.0"
files = [
@@ -360,6 +609,7 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
name = "docker"
version = "6.1.3"
description = "A Python library for the Docker Engine API."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -379,13 +629,14 @@ ssh = ["paramiko (>=2.4.3)"]
[[package]]
name = "exceptiongroup"
-version = "1.1.2"
+version = "1.1.3"
description = "Backport of PEP 654 (exception groups)"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"},
- {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"},
+ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
+ {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
]
[package.extras]
@@ -395,6 +646,7 @@ test = ["pytest (>=6)"]
name = "fastapi"
version = "0.95.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -412,10 +664,28 @@ dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>
doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"]
test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
+[[package]]
+name = "filelock"
+version = "3.12.4"
+description = "A platform independent file lock."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
+ {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
+typing = ["typing-extensions (>=4.7.1)"]
+
[[package]]
name = "frozenlist"
version = "1.4.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
+category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -484,81 +754,85 @@ files = [
[[package]]
name = "greenlet"
-version = "2.0.2"
+version = "3.0.0"
description = "Lightweight in-process concurrent programming"
+category = "main"
optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
-files = [
- {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"},
- {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"},
- {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
- {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
- {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
- {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
- {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"},
- {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"},
- {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
- {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
- {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
- {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
- {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"},
- {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"},
- {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"},
- {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"},
- {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"},
- {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"},
- {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"},
- {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"},
- {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"},
- {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"},
- {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"},
- {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"},
- {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"},
- {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"},
- {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"},
- {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"},
- {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"},
- {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
- {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
- {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
- {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"},
- {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"},
- {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
- {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
- {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
- {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"},
- {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"},
- {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"},
- {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"},
- {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"},
- {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"},
- {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"},
+python-versions = ">=3.7"
+files = [
+ {file = "greenlet-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e09dea87cc91aea5500262993cbd484b41edf8af74f976719dd83fe724644cd6"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47932c434a3c8d3c86d865443fadc1fbf574e9b11d6650b656e602b1797908a"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfaeecf8cc705d35d8e6de324bf58427d7eafb55f67050d8f28053a3d57118c"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a68d670c8f89ff65c82b936275369e532772eebc027c3be68c6b87ad05ca695"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ad562a104cd41e9d4644f46ea37167b93190c6d5e4048fcc4b80d34ecb278f"},
+ {file = "greenlet-3.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a807b2a58d5cdebb07050efe3d7deaf915468d112dfcf5e426d0564aa3aa4a"},
+ {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1660a15a446206c8545edc292ab5c48b91ff732f91b3d3b30d9a915d5ec4779"},
+ {file = "greenlet-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:813720bd57e193391dfe26f4871186cf460848b83df7e23e6bef698a7624b4c9"},
+ {file = "greenlet-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa15a2ec737cb609ed48902b45c5e4ff6044feb5dcdfcf6fa8482379190330d7"},
+ {file = "greenlet-3.0.0-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:7709fd7bb02b31908dc8fd35bfd0a29fc24681d5cc9ac1d64ad07f8d2b7db62f"},
+ {file = "greenlet-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:211ef8d174601b80e01436f4e6905aca341b15a566f35a10dd8d1e93f5dbb3b7"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6512592cc49b2c6d9b19fbaa0312124cd4c4c8a90d28473f86f92685cc5fef8e"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871b0a8835f9e9d461b7fdaa1b57e3492dd45398e87324c047469ce2fc9f516c"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b505fcfc26f4148551826a96f7317e02c400665fa0883fe505d4fcaab1dabfdd"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123910c58234a8d40eaab595bc56a5ae49bdd90122dde5bdc012c20595a94c14"},
+ {file = "greenlet-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96d9ea57292f636ec851a9bb961a5cc0f9976900e16e5d5647f19aa36ba6366b"},
+ {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b72b802496cccbd9b31acea72b6f87e7771ccfd7f7927437d592e5c92ed703c"},
+ {file = "greenlet-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:527cd90ba3d8d7ae7dceb06fda619895768a46a1b4e423bdb24c1969823b8362"},
+ {file = "greenlet-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:37f60b3a42d8b5499be910d1267b24355c495064f271cfe74bf28b17b099133c"},
+ {file = "greenlet-3.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1482fba7fbed96ea7842b5a7fc11d61727e8be75a077e603e8ab49d24e234383"},
+ {file = "greenlet-3.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:be557119bf467d37a8099d91fbf11b2de5eb1fd5fc5b91598407574848dc910f"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b2f1922a39d5d59cc0e597987300df3396b148a9bd10b76a058a2f2772fc04"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1e22c22f7826096ad503e9bb681b05b8c1f5a8138469b255eb91f26a76634f2"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d363666acc21d2c204dd8705c0e0457d7b2ee7a76cb16ffc099d6799744ac99"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:334ef6ed8337bd0b58bb0ae4f7f2dcc84c9f116e474bb4ec250a8bb9bd797a66"},
+ {file = "greenlet-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6672fdde0fd1a60b44fb1751a7779c6db487e42b0cc65e7caa6aa686874e79fb"},
+ {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:952256c2bc5b4ee8df8dfc54fc4de330970bf5d79253c863fb5e6761f00dda35"},
+ {file = "greenlet-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:269d06fa0f9624455ce08ae0179430eea61085e3cf6457f05982b37fd2cefe17"},
+ {file = "greenlet-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9adbd8ecf097e34ada8efde9b6fec4dd2a903b1e98037adf72d12993a1c80b51"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b5ce7f40f0e2f8b88c28e6691ca6806814157ff05e794cdd161be928550f4c"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf94aa539e97a8411b5ea52fc6ccd8371be9550c4041011a091eb8b3ca1d810"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80dcd3c938cbcac986c5c92779db8e8ce51a89a849c135172c88ecbdc8c056b7"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52a712c38e5fb4fd68e00dc3caf00b60cb65634d50e32281a9d6431b33b4af1"},
+ {file = "greenlet-3.0.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5539f6da3418c3dc002739cb2bb8d169056aa66e0c83f6bacae0cd3ac26b423"},
+ {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:343675e0da2f3c69d3fb1e894ba0a1acf58f481f3b9372ce1eb465ef93cf6fed"},
+ {file = "greenlet-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:abe1ef3d780de56defd0c77c5ba95e152f4e4c4e12d7e11dd8447d338b85a625"},
+ {file = "greenlet-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:e693e759e172fa1c2c90d35dea4acbdd1d609b6936115d3739148d5e4cd11947"},
+ {file = "greenlet-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bdd696947cd695924aecb3870660b7545a19851f93b9d327ef8236bfc49be705"},
+ {file = "greenlet-3.0.0-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:cc3e2679ea13b4de79bdc44b25a0c4fcd5e94e21b8f290791744ac42d34a0353"},
+ {file = "greenlet-3.0.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:63acdc34c9cde42a6534518e32ce55c30f932b473c62c235a466469a710bfbf9"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a1a6244ff96343e9994e37e5b4839f09a0207d35ef6134dce5c20d260d0302c"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b822fab253ac0f330ee807e7485769e3ac85d5eef827ca224feaaefa462dc0d0"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8060b32d8586e912a7b7dac2d15b28dbbd63a174ab32f5bc6d107a1c4143f40b"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621fcb346141ae08cb95424ebfc5b014361621b8132c48e538e34c3c93ac7365"},
+ {file = "greenlet-3.0.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb36985f606a7c49916eff74ab99399cdfd09241c375d5a820bb855dfb4af9f"},
+ {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10b5582744abd9858947d163843d323d0b67be9432db50f8bf83031032bc218d"},
+ {file = "greenlet-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f351479a6914fd81a55c8e68963609f792d9b067fb8a60a042c585a621e0de4f"},
+ {file = "greenlet-3.0.0-cp38-cp38-win32.whl", hash = "sha256:9de687479faec7db5b198cc365bc34addd256b0028956501f4d4d5e9ca2e240a"},
+ {file = "greenlet-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:3fd2b18432e7298fcbec3d39e1a0aa91ae9ea1c93356ec089421fabc3651572b"},
+ {file = "greenlet-3.0.0-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:3c0d36f5adc6e6100aedbc976d7428a9f7194ea79911aa4bf471f44ee13a9464"},
+ {file = "greenlet-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4cd83fb8d8e17633ad534d9ac93719ef8937568d730ef07ac3a98cb520fd93e4"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a5b2d4cdaf1c71057ff823a19d850ed5c6c2d3686cb71f73ae4d6382aaa7a06"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e7dcdfad252f2ca83c685b0fa9fba00e4d8f243b73839229d56ee3d9d219314"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94e4e924d09b5a3e37b853fe5924a95eac058cb6f6fb437ebb588b7eda79870"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad6fb737e46b8bd63156b8f59ba6cdef46fe2b7db0c5804388a2d0519b8ddb99"},
+ {file = "greenlet-3.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d55db1db455c59b46f794346efce896e754b8942817f46a1bada2d29446e305a"},
+ {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56867a3b3cf26dc8a0beecdb4459c59f4c47cdd5424618c08515f682e1d46692"},
+ {file = "greenlet-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a812224a5fb17a538207e8cf8e86f517df2080c8ee0f8c1ed2bdaccd18f38f4"},
+ {file = "greenlet-3.0.0-cp39-cp39-win32.whl", hash = "sha256:0d3f83ffb18dc57243e0151331e3c383b05e5b6c5029ac29f754745c800f8ed9"},
+ {file = "greenlet-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:831d6f35037cf18ca5e80a737a27d822d87cd922521d18ed3dbc8a6967be50ce"},
+ {file = "greenlet-3.0.0-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:a048293392d4e058298710a54dfaefcefdf49d287cd33fb1f7d63d55426e4355"},
+ {file = "greenlet-3.0.0.tar.gz", hash = "sha256:19834e3f91f485442adc1ee440171ec5d9a4840a1f7bd5ed97833544719ce10b"},
]
[package.extras]
-docs = ["Sphinx", "docutils (<0.18)"]
+docs = ["Sphinx"]
test = ["objgraph", "psutil"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -570,6 +844,7 @@ files = [
name = "httpcore"
version = "0.17.3"
description = "A minimal low-level HTTP client."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -581,16 +856,17 @@ files = [
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
-sniffio = "==1.*"
+sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.24.1"
description = "The next generation HTTP client."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -606,14 +882,15 @@ sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
-cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
-socks = ["socksio (==1.*)"]
+socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -625,6 +902,7 @@ files = [
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -632,10 +910,23 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
+[[package]]
+name = "jmespath"
+version = "1.0.1"
+description = "JSON Matching Expressions"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
+ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
+]
+
[[package]]
name = "langchain"
version = "0.0.256"
description = "Building applications with LLMs through composability"
+category = "main"
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
@@ -673,23 +964,25 @@ text-helpers = ["chardet (>=5.1.0,<6.0.0)"]
[[package]]
name = "langsmith"
-version = "0.0.21"
+version = "0.0.47"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
+category = "main"
optional = false
python-versions = ">=3.8.1,<4.0"
files = [
- {file = "langsmith-0.0.21-py3-none-any.whl", hash = "sha256:a04c6eb3b4fc6205b15a559705f726fd0114ee2b3bd8668a0bd11cf29d5c5992"},
- {file = "langsmith-0.0.21.tar.gz", hash = "sha256:ec90ddab6beee6c344cf0ed8ae7d68948740cf98e119dd97c571f3190555644e"},
+ {file = "langsmith-0.0.47-py3-none-any.whl", hash = "sha256:90279d4888e4513f83703becf38a6d92b7c7d696b120872fa1fb28c5c67bfd91"},
+ {file = "langsmith-0.0.47.tar.gz", hash = "sha256:04b8373cbfcf7b9c28ba53174206095c50dc09ae1131a99b501f92776c8ad0c8"},
]
[package.dependencies]
-pydantic = ">=1,<2"
+pydantic = ">=1,<3"
requests = ">=2,<3"
[[package]]
name = "marshmallow"
version = "3.20.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
+category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -710,6 +1003,7 @@ tests = ["pytest", "pytz", "simplejson"]
name = "marshmallow-enum"
version = "1.5.1"
description = "Enum field for Marshmallow"
+category = "main"
optional = false
python-versions = "*"
files = [
@@ -722,31 +1016,33 @@ marshmallow = ">=2.0.0"
[[package]]
name = "motor"
-version = "3.2.0"
+version = "3.1.2"
description = "Non-blocking MongoDB driver for Tornado or asyncio"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "motor-3.2.0-py3-none-any.whl", hash = "sha256:82cd3d8a3b57e322c3fa382a393b52828c9a2e98b315c78af36f01bae78af6a6"},
- {file = "motor-3.2.0.tar.gz", hash = "sha256:4fb1e8502260f853554f24115421584e83904a6debb577354d33e9711ee99008"},
+ {file = "motor-3.1.2-py3-none-any.whl", hash = "sha256:4bfc65230853ad61af447088527c1197f91c20ee957cfaea3144226907335716"},
+ {file = "motor-3.1.2.tar.gz", hash = "sha256:80c08477c09e70db4f85c99d484f2bafa095772f1d29b3ccb253270f9041da9a"},
]
[package.dependencies]
-pymongo = ">=4.4,<5"
+pymongo = ">=4.1,<5"
[package.extras]
-aws = ["pymongo[aws] (>=4.4,<5)"]
-encryption = ["pymongo[encryption] (>=4.4,<5)"]
-gssapi = ["pymongo[gssapi] (>=4.4,<5)"]
-ocsp = ["pymongo[ocsp] (>=4.4,<5)"]
-snappy = ["pymongo[snappy] (>=4.4,<5)"]
-srv = ["pymongo[srv] (>=4.4,<5)"]
-zstd = ["pymongo[zstd] (>=4.4,<5)"]
+aws = ["pymongo[aws] (>=4.1,<5)"]
+encryption = ["pymongo[encryption] (>=4.1,<5)"]
+gssapi = ["pymongo[gssapi] (>=4.1,<5)"]
+ocsp = ["pymongo[ocsp] (>=4.1,<5)"]
+snappy = ["pymongo[snappy] (>=4.1,<5)"]
+srv = ["pymongo[srv] (>=4.1,<5)"]
+zstd = ["pymongo[zstd] (>=4.1,<5)"]
[[package]]
name = "multidict"
version = "6.0.4"
description = "multidict implementation"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -830,6 +1126,7 @@ files = [
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
+category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -839,41 +1136,41 @@ files = [
[[package]]
name = "numexpr"
-version = "2.8.5"
+version = "2.8.7"
description = "Fast numerical expression evaluator for NumPy"
+category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
files = [
- {file = "numexpr-2.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51f3ab160c3847ebcca93cd88f935a7802b54a01ab63fe93152994a64d7a6cf2"},
- {file = "numexpr-2.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:de29c77f674e4eb8f0846525a475cab64008c227c8bc4ba5153ab3f72441cc63"},
- {file = "numexpr-2.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf85ba1327eb87ec82ae7936f13c8850fb969a0ca34f3ba9fa3897c09d5c80d7"},
- {file = "numexpr-2.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c00be69f747f44a631830215cab482f0f77f75af2925695adff57c1cc0f9a68"},
- {file = "numexpr-2.8.5-cp310-cp310-win32.whl", hash = "sha256:c46350dcdb93e32f033eea5a21269514ffcaf501d9abd6036992d37e48a308b0"},
- {file = "numexpr-2.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:894b027438b8ec88dea32a19193716c79f4ff8ddb92302dcc9731b51ba3565a8"},
- {file = "numexpr-2.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6df184d40d4cf9f21c71f429962f39332f7398147762588c9f3a5c77065d0c06"},
- {file = "numexpr-2.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:178b85ad373c6903e55d75787d61b92380439b70d94b001cb055a501b0821335"},
- {file = "numexpr-2.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:578fe4008e4d5d6ff01bbeb2d7b7ba1ec658a5cda9c720cd26a9a8325f8ef438"},
- {file = "numexpr-2.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef621b4ee366a5c6a484f6678c9259f5b826569f8bfa0b89ba2306d5055468bb"},
- {file = "numexpr-2.8.5-cp311-cp311-win32.whl", hash = "sha256:dd57ab1a3d3aaa9274aff1cefbf93b8ddacc7973afef5b125905f6bf18fabab0"},
- {file = "numexpr-2.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:783324ba40eb804ecfc9ebae86120a1e339ab112d0ab8a1f0d48a26354d5bf9b"},
- {file = "numexpr-2.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:183d5430db76826e54465c69db93a3c6ecbf03cda5aa1bb96eaad0147e9b68dc"},
- {file = "numexpr-2.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39ce106f92ccea5b07b1d6f2f3c4370f05edf27691dc720a63903484a2137e48"},
- {file = "numexpr-2.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b594dc9e2d6291a0bc5c065e6d9caf3eee743b5663897832e9b17753c002947a"},
- {file = "numexpr-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:62b4faf8e0627673b0210a837792bddd23050ecebc98069ab23eb0633ff1ef5f"},
- {file = "numexpr-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:db5c65417d69414f1ab31302ea01d3548303ef31209c38b4849d145be4e1d1ba"},
- {file = "numexpr-2.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb36ffcfa1606e41aa08d559b4277bcad0e16b83941d1a4fee8d2bd5a34f8e0e"},
- {file = "numexpr-2.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34af2a0e857d02a4bc5758bc037a777d50dacb13bcd57c7905268a3e44994ed6"},
- {file = "numexpr-2.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a8dad2bfaad5a5c34a2e8bbf62b9df1dfab266d345fda1feb20ff4e264b347a"},
- {file = "numexpr-2.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93f5a866cd13a808bc3d3a9c487d94cd02eec408b275ff0aa150f2e8e5191f8"},
- {file = "numexpr-2.8.5-cp38-cp38-win32.whl", hash = "sha256:558390fea6370003ac749ed9d0f38d708aa096f5dcb707ddb6e0ca5a0dd37da1"},
- {file = "numexpr-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:55983806815035eb63c5039520688c49536bb7f3cc3fc1d7d64c6a00cf3f353e"},
- {file = "numexpr-2.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1510da20e6f5f45333610b1ded44c566e2690c6c437c84f2a212ca09627c7e01"},
- {file = "numexpr-2.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e8b5bf7bcb4e8dcd66522d8fc96e1db7278f901cb4fd2e155efbe62a41dde08"},
- {file = "numexpr-2.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ed0e1c1ef5f34381448539f1fe9015906d21c9cfa2797c06194d4207dadb465"},
- {file = "numexpr-2.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aea6ab45c87c0a7041183c08a798f0ad4d7c5eccbce20cfe79ce6f1a45ef3702"},
- {file = "numexpr-2.8.5-cp39-cp39-win32.whl", hash = "sha256:cbfd833ee5fdb0efb862e152aee7e6ccea9c596d5c11d22604c2e6307bff7cad"},
- {file = "numexpr-2.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:283ce8609a7ccbadf91a68f3484558b3e36d27c93c98a41ec205efb0ab43c872"},
- {file = "numexpr-2.8.5.tar.gz", hash = "sha256:45ed41e55a0abcecf3d711481e12a5fb7a904fe99d42bc282a17cc5f8ea510be"},
+ {file = "numexpr-2.8.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d88531ffea3ea9287e8a1665c6a2d0206d3f4660d5244423e2a134a7f0ce5fba"},
+ {file = "numexpr-2.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db1065ba663a854115cf1f493afd7206e2efcef6643129e8061e97a51ad66ebb"},
+ {file = "numexpr-2.8.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4546416004ff2e7eb9cf52c2d7ab82732b1b505593193ee9f93fa770edc5230"},
+ {file = "numexpr-2.8.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb2f473fdfd09d17db3038e34818d05b6bc561a36785aa927d6c0e06bccc9911"},
+ {file = "numexpr-2.8.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5496fc9e3ae214637cbca1ab556b0e602bd3afe9ff4c943a29c482430972cda8"},
+ {file = "numexpr-2.8.7-cp310-cp310-win32.whl", hash = "sha256:d43f1f0253a6f2db2f76214e6f7ae9611b422cba3f7d4c86415d7a78bbbd606f"},
+ {file = "numexpr-2.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:cf5f112bce5c5966c47cc33700bc14ce745c8351d437ed57a9574fff581f341a"},
+ {file = "numexpr-2.8.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32934d51b5bc8a6636436326da79ed380e2f151989968789cf65b1210572cb46"},
+ {file = "numexpr-2.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f021ac93cb3dd5d8ba2882627b615b1f58cb089dcc85764c6fbe7a549ed21b0c"},
+ {file = "numexpr-2.8.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccf572763517db6562fb7b17db46aacbbf62a9ca0a66672872f4f71aee7b186"},
+ {file = "numexpr-2.8.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11121b14ee3179bade92e823f25f1b94e18716d33845db5081973331188c3338"},
+ {file = "numexpr-2.8.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81451962d4145a46dba189df65df101d4d1caddb6efe6ebfe05982cd9f62b2cf"},
+ {file = "numexpr-2.8.7-cp311-cp311-win32.whl", hash = "sha256:da55ba845b847cc33c4bf81cee4b1bddfb0831118cabff8db62888ab8697ec34"},
+ {file = "numexpr-2.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:fd93b88d5332069916fa00829ea1b972b7e73abcb1081eee5c905a514b8b59e3"},
+ {file = "numexpr-2.8.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5340d2c86d83f52e1a3e7fd97c37d358ae99af9de316bdeeab2565b9b1e622ca"},
+ {file = "numexpr-2.8.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3bdf8cbc00c77a46230c765d242f92d35905c239b20c256c48dbac91e49f253"},
+ {file = "numexpr-2.8.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46c47e361fa60966a3339cb4f463ae6151ce7d78ed38075f06e8585d2c8929f"},
+ {file = "numexpr-2.8.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a371cfc1670a18eea2d5c70abaa95a0e8824b70d28da884bad11931266e3a0ca"},
+ {file = "numexpr-2.8.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:47a249cecd1382d482a5bf1fac0d11392fb2ed0f7d415ebc4cd901959deb1ec9"},
+ {file = "numexpr-2.8.7-cp312-cp312-win32.whl", hash = "sha256:b8a5b2c21c26b62875bf819d375d798b96a32644e3c28bd4ce7789ed1fb489da"},
+ {file = "numexpr-2.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:f29f4d08d9b0ed6fa5d32082971294b2f9131b8577c2b7c36432ed670924313f"},
+ {file = "numexpr-2.8.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ecaa5be24cf8fa0f00108e9dfa1021b7510e9dd9d159b8d8bc7c7ddbb995b31"},
+ {file = "numexpr-2.8.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a84284e0a407ca52980fd20962e89aff671c84cd6e73458f2e29ea2aa206356"},
+ {file = "numexpr-2.8.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e838289e3b7bbe100b99e35496e6cc4cc0541c2207078941ee5a1d46e6b925ae"},
+ {file = "numexpr-2.8.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0983052f308ea75dd232eb7f4729eed839db8fe8d82289940342b32cc55b15d0"},
+ {file = "numexpr-2.8.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8bf005acd7f1985c71b1b247aaac8950d6ea05a0fe0bbbbf3f96cd398b136daa"},
+ {file = "numexpr-2.8.7-cp39-cp39-win32.whl", hash = "sha256:56ec95f8d1db0819e64987dcf1789acd500fa4ea396eeabe4af6efdcb8902d07"},
+ {file = "numexpr-2.8.7-cp39-cp39-win_amd64.whl", hash = "sha256:c7bf60fc1a9c90a9cb21c4c235723e579bff70c8d5362228cb2cf34426104ba2"},
+ {file = "numexpr-2.8.7.tar.gz", hash = "sha256:596eeb3bbfebc912f4b6eaaf842b61ba722cebdb8bc42dfefa657d3a74953849"},
]
[package.dependencies]
@@ -883,6 +1180,7 @@ numpy = ">=1.13.3"
name = "numpy"
version = "1.25.2"
description = "Fundamental package for array computing in Python"
+category = "main"
optional = false
python-versions = ">=3.9"
files = [
@@ -913,15 +1211,40 @@ files = [
{file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"},
]
+[[package]]
+name = "odmantic"
+version = "0.9.2"
+description = "ODMantic, an AsyncIO MongoDB Object Document Mapper for Python using type hints "
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "odmantic-0.9.2-py3-none-any.whl", hash = "sha256:a82f4fe6a6face24cf0cd0738c9bb32564219cb5a5ed6b0a58e92a4c2048e4d8"},
+ {file = "odmantic-0.9.2.tar.gz", hash = "sha256:9780087e1bc2afbd0c1f16b2d18137889dbe7e0df12af0762d6b6b17dadd36be"},
+]
+
+[package.dependencies]
+motor = ">=2.1.0,<3.2.0"
+pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1"
+pymongo = ">=3.11.0,<5.0.0"
+typing-extensions = {version = ">=4.2.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["ipython (>=7.16.1,<7.17.0)"]
+doc = ["mkdocs-macros-plugin (>=0.5.0,<0.6.0)", "mkdocs-material (>=8.4.0,<8.5.0)", "mkdocstrings[python] (>=0.19.0,<0.20.0)", "pydocstyle[toml] (>=6.1.1,<6.2.0)"]
+fastapi = ["fastapi (>=0.61.1)"]
+test = ["async-asgi-testclient (>=1.4.4,<1.5.0)", "asyncmock (>=0.4.2,<0.5.0)", "black (>=22.3.0,<22.4.0)", "coverage[toml] (>=6.2,<7.0)", "darglint (>=1.8.1,<1.9.0)", "fastapi (>=0.61.1,<0.69.0)", "isort (>=5.8.0,<5.9.0)", "mypy (>=0.961,<1.0)", "pytest (>=7.0,<8.0)", "pytest-asyncio (>=0.16.0,<0.17.0)", "pytest-sugar (>=0.9.5,<0.10.0)", "pytest-xdist (>=2.1.0,<2.2.0)", "pytz (>=2022.1,<2023.0)", "requests (>=2.24.0,<2.25.0)", "ruff (>=0.0.137,<0.1.0)", "semver (>=2.13.0,<2.14.0)", "typer (>=0.4.1,<0.5.0)", "types-pytz (>=2022.1.1,<2022.2.0)", "uvicorn (>=0.17.0,<0.18.0)"]
+
[[package]]
name = "openai"
-version = "0.27.8"
+version = "0.27.10"
description = "Python client library for the OpenAI API"
+category = "main"
optional = false
python-versions = ">=3.7.1"
files = [
- {file = "openai-0.27.8-py3-none-any.whl", hash = "sha256:e0a7c2f7da26bdbe5354b03c6d4b82a2f34bd4458c7a17ae1a7092c3e397e03c"},
- {file = "openai-0.27.8.tar.gz", hash = "sha256:2483095c7db1eee274cebac79e315a986c4e55207bb4fa7b82d185b3a2ed9536"},
+ {file = "openai-0.27.10-py3-none-any.whl", hash = "sha256:beabd1757e3286fa166dde3b70ebb5ad8081af046876b47c14c41e203ed22a14"},
+ {file = "openai-0.27.10.tar.gz", hash = "sha256:60e09edf7100080283688748c6803b7b3b52d5a55d21890f3815292a0552d83b"},
]
[package.dependencies]
@@ -931,7 +1254,7 @@ tqdm = "*"
[package.extras]
datalib = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
-dev = ["black (>=21.6b0,<22.0)", "pytest (==6.*)", "pytest-asyncio", "pytest-mock"]
+dev = ["black (>=21.6b0,<22.0)", "pytest (>=6.0.0,<7.0.0)", "pytest-asyncio", "pytest-mock"]
embeddings = ["matplotlib", "numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "plotly", "scikit-learn (>=1.0.2)", "scipy", "tenacity (>=8.0.1)"]
wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)", "wandb"]
@@ -939,6 +1262,7 @@ wandb = ["numpy", "openpyxl (>=3.0.7)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1
name = "openapi-schema-pydantic"
version = "1.2.4"
description = "OpenAPI (v3) specification schema as pydantic class"
+category = "main"
optional = false
python-versions = ">=3.6.1"
files = [
@@ -951,73 +1275,152 @@ pydantic = ">=1.8.2"
[[package]]
name = "packaging"
-version = "23.1"
+version = "23.2"
description = "Core utilities for Python packages"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
- {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "phonenumbers"
+version = "8.12.48"
+description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "phonenumbers-8.12.48-py2.py3-none-any.whl", hash = "sha256:429273b98966475d0c18ee293096eaf81c6b5727d0d55c7ba5ce9c60ec8c59ef"},
+ {file = "phonenumbers-8.12.48.tar.gz", hash = "sha256:af0681fbfe9fa0721376ad9b729e772e5d20bf2cf50d9dd8ca2f0bdd78e9f0ce"},
+]
+
+[[package]]
+name = "pkce"
+version = "1.0.3"
+description = "PKCE Pyhton generator."
+category = "main"
+optional = false
+python-versions = ">=3"
+files = [
+ {file = "pkce-1.0.3-py3-none-any.whl", hash = "sha256:55927e24c7d403b2491ebe182b95d9dcb1807643243d47e3879fbda5aad4471d"},
+ {file = "pkce-1.0.3.tar.gz", hash = "sha256:9775fd76d8a743d39b87df38af1cd04a58c9b5a5242d5a6350ef343d06814ab6"},
]
[[package]]
name = "pluggy"
-version = "1.2.0"
+version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
+category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
- {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
+ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
+ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+
+[[package]]
+name = "pycryptodome"
+version = "3.10.4"
+description = "Cryptographic library for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "pycryptodome-3.10.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:91ba4215a1f37d0f371fe43bc88c5ff49c274849f3868321c889313787de7672"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:66301e4c42dee43ee2da256625d3fe81ef98cc9924c2bd535008cc3ad8ded77b"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:8ec154ec445412df31acf0096e7f715e30e167c8f2318b8f5b1ab7c28f4c82f7"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:8e82524e7c354033508891405574d12e612cc4fdd3b55d2c238fc1a3e300b606"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b217b4525e60e1af552d62bec01b4685095436d4de5ecde0f05d75b2f95ba6d4"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:3a153658d97258ca20bf18f7fe31c09cc7c558b6f8974a6ec74e19f6c634bd64"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-win32.whl", hash = "sha256:c6469d1453f5864e3321a172b0aa671b938d753cbf2376b99fa2ab8841539bb8"},
+ {file = "pycryptodome-3.10.4-cp27-cp27m-win_amd64.whl", hash = "sha256:6b45fcace5a5d9c57ba87cf804b161adc62aa826295ce7f7acbcbdc0df74ed37"},
+ {file = "pycryptodome-3.10.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b1daf251395af7336ddde6a0015ba5e632c18fe646ba930ef87402537358e3b4"},
+ {file = "pycryptodome-3.10.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9a2312440057bf29b9582f72f14d79692044e63bfbc4b4bbea8559355f44f3dd"},
+ {file = "pycryptodome-3.10.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:54d4e4d45f349d8c4e2f31c2734637ff62a844af391b833f789da88e43a8f338"},
+ {file = "pycryptodome-3.10.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:97e7df67a4da2e3f60612bbfd6c3f243a63a15d8f4797dd275e1d7b44a65cb12"},
+ {file = "pycryptodome-3.10.4-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:db15fa07d2a4c00beeb5e9acdfdbc1c79f9ccfbdc1a8f36c82c4aa44951b33c9"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:217dcc0c92503f7dd4b3d3b7d974331a4419f97f555c99a845c3b366fed7056b"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-manylinux1_i686.whl", hash = "sha256:a7471646d8cd1a58bb696d667dcb3853e5c9b341b68dcf3c3cc0893d0f98ca5f"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:d713dc0910e5ded07852a05e9b75f1dd9d3a31895eebee0668f612779b2a748c"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:ac3012c36633564b2b5539bb7c6d9175f31d2ce74844e9abe654c428f02d0fd8"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:3f9fb499e267039262569d08658132c9cd8b136bf1d8c56b72f70ed05551e526"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:309529d2526f3fb47102aeef376b3459110a6af7efb162e860b32e3a17a46f06"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-win32.whl", hash = "sha256:7efec2418e9746ec48e264eea431f8e422d931f71c57b1c96ee202b117f58fa9"},
+ {file = "pycryptodome-3.10.4-cp35-abi3-win_amd64.whl", hash = "sha256:49e54f2245befb0193848c8c8031d8d1358ed4af5a1ae8d0a3ba669a5cdd3a72"},
+ {file = "pycryptodome-3.10.4-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:4e8fc4c48365ce8a542fe48bf1360da05bb2851df12f64fc94d751705e7cdbe7"},
+ {file = "pycryptodome-3.10.4-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:851e6d4930b160417235955322db44adbdb19589918670d63f4acd5d92959ac0"},
+ {file = "pycryptodome-3.10.4-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:04e14c732c3693d2830839feed5129286ce47ffa8bfe90e4ae042c773e51c677"},
+ {file = "pycryptodome-3.10.4-pp27-pypy_73-win32.whl", hash = "sha256:cefe6b267b8e5c3c72e11adec35a9c7285b62e8ea141b63e87055e9a9e5f2f8c"},
+ {file = "pycryptodome-3.10.4-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:24c1b7705d19d8ae3e7255431efd2e526006855df62620118dd7b5374c6372f6"},
+ {file = "pycryptodome-3.10.4-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:c61ea053bd5d4c12a063d7e704fbe1c45abb5d2510dab55bd95d166ba661604f"},
+ {file = "pycryptodome-3.10.4-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:11d3164fb49fdee000fde05baecce103c0c698168ef1a18d9c7429dd66f0f5bb"},
+ {file = "pycryptodome-3.10.4-pp36-pypy36_pp73-win32.whl", hash = "sha256:3faa6ebd35c61718f3f8862569c1f38450c24f3ededb213e1a64806f02f584bc"},
+ {file = "pycryptodome-3.10.4.tar.gz", hash = "sha256:40083b0d7f277452c7f2dd4841801f058cc12a74c219ee4110d65774c6a58bef"},
+]
+
[[package]]
name = "pydantic"
-version = "1.10.12"
+version = "1.10.13"
description = "Data validation and settings management using python type hints"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"},
- {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"},
- {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"},
- {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"},
- {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"},
- {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"},
- {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"},
- {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"},
- {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"},
- {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"},
- {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"},
- {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"},
- {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"},
- {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"},
- {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"},
- {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"},
- {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"},
- {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"},
- {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"},
- {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"},
- {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"},
- {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"},
- {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"},
- {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"},
- {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"},
- {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"},
- {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"},
- {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"},
- {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"},
- {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"},
- {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"},
- {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"},
- {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"},
- {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"},
- {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"},
- {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"},
+ {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"},
+ {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"},
+ {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"},
+ {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"},
+ {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"},
+ {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"},
+ {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"},
+ {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"},
+ {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"},
+ {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"},
+ {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"},
+ {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"},
+ {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"},
+ {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"},
+ {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"},
+ {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"},
+ {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"},
+ {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"},
+ {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"},
+ {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"},
+ {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"},
+ {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"},
+ {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"},
+ {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"},
+ {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"},
+ {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"},
+ {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"},
+ {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"},
+ {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"},
+ {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"},
+ {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"},
+ {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"},
+ {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"},
+ {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"},
+ {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"},
+ {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"},
]
[package.dependencies]
@@ -1027,87 +1430,116 @@ typing-extensions = ">=4.2.0"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
+[[package]]
+name = "pyjwt"
+version = "2.8.0"
+description = "JSON Web Token implementation in Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
+ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
[[package]]
name = "pymongo"
-version = "4.4.1"
+version = "4.5.0"
description = "Python driver for MongoDB "
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pymongo-4.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bbdd6c719cc2ea440d7245ba71ecdda507275071753c6ffe9c8232647246f575"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:a438508dd8007a4a724601c3790db46fe0edc3d7d172acafc5f148ceb4a07815"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:3a350d03959f9d5b7f2ea0621f5bb2eb3927b8fc1c4031d12cfd3949839d4f66"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:e6d5d2c97c35f83dc65ccd5d64c7ed16eba6d9403e3744e847aee648c432f0bb"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:1944b16ffef3573ae064196460de43eb1c865a64fed23551b5eac1951d80acca"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:912b0fdc16500125dc1837be8b13c99d6782d93d6cd099d0e090e2aca0b6d100"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:d1b1c8eb21de4cb5e296614e8b775d5ecf9c56b7d3c6000f4bfdb17f9e244e72"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3b508e0de613b906267f2c484cb5e9afd3a64680e1af23386ca8f99a29c6145"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41feb8cf429799ac43ed34504839954aa7d907f8bd9ecb52ed5ff0d2ea84245"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1897123c4bede1af0c264a3bc389a2505bae50d85e4f211288d352928c02d017"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c4bcd285bf0f5272d50628e4ea3989738e3af1251b2dd7bf50da2d593f3a56"},
- {file = "pymongo-4.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:995b868ccc9df8d36cb28142363e3911846fe9f43348d942951f60cdd7f62224"},
- {file = "pymongo-4.4.1-cp310-cp310-win32.whl", hash = "sha256:a5198beca36778f19a98b56f541a0529502046bc867b352dda5b6322e1ddc4fd"},
- {file = "pymongo-4.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:a86d20210c9805a032cda14225087ec483613aff0955327c7871a3c980562c5b"},
- {file = "pymongo-4.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5a2a1da505ea78787b0382c92dc21a45d19918014394b220c4734857e9c73694"},
- {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35545583396684ea70a0b005034a469bf3f447732396e5b3d50bec94890b8d5c"},
- {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5248fdf7244a5e976279fe154d116c73f6206e0be71074ea9d9b1e73b5893dd5"},
- {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44381b817eeb47a41bbfbd279594a7fb21017e0e3e15550eb0fd3758333097f3"},
- {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f0bd25de90b804cc95e548f55f430df2b47f242a4d7bbce486db62f3b3c981f"},
- {file = "pymongo-4.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d67f4029c57b36a0278aeae044ce382752c078c7625cef71b5e2cf3e576961f9"},
- {file = "pymongo-4.4.1-cp311-cp311-win32.whl", hash = "sha256:8082eef0d8c711c9c272906fa469965e52b44dbdb8a589b54857b1351dc2e511"},
- {file = "pymongo-4.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:980da627edc1275896d7d4670596433ec66e1f452ec244e07bbb2f91c955b581"},
- {file = "pymongo-4.4.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:6cf08997d3ecf9a1eabe12c35aa82a5c588f53fac054ed46fe5c16a0a20ea43d"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a6750449759f0a83adc9df3a469483a8c3eef077490b76f30c03dc8f7a4b1d66"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:efa67f46c1678df541e8f41247d22430905f80a3296d9c914aaa793f2c9fa1db"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d9a5e16a32fb1000c72a8734ddd8ae291974deb5d38d40d1bdd01dbe4024eeb0"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:36b0b06c6e830d190215fced82872e5fd8239771063afa206f9adc09574018a3"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4ec9c6d4547c93cf39787c249969f7348ef6c4d36439af10d57b5ee65f3dfbf9"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:5368801ca6b66aacc5cc013258f11899cd6a4c3bb28cec435dd67f835905e9d2"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:91848d555155ad4594de5e575b6452adc471bc7bc4b4d2b1f4f15a78a8af7843"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0f08a2dba1469252462c414b66cb416c7f7295f2c85e50f735122a251fcb131"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fe4bbf2b2c91e4690b5658b0fbb98ca6e0a8fba9ececd65b4e7d2d1df3e9b01"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e307d67641d0e2f7e7d6ee3dad880d090dace96cc1d95c99d15bd9f545a1168"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d43634594f2486cc9bb604a1dc0914234878c4faf6604574a25260cb2faaa06"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef0e3279e72cccc3dc7be75b12b1e54cc938d7ce13f5f22bea844b9d9d5fecd4"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05935f5a4bbae0a99482147588351b7b17999f4a4e6e55abfb74367ac58c0634"},
- {file = "pymongo-4.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:854d92d2437e3496742e17342496e1f3d9efb22455501fd6010aa3658138e457"},
- {file = "pymongo-4.4.1-cp37-cp37m-win32.whl", hash = "sha256:ddffc0c6d0e92cf43dc6c47639d1ef9ab3c280db2998a33dbb9953bd864841e1"},
- {file = "pymongo-4.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2259302d8ab51cd56c3d9d5cca325977e35a0bb3a15a297ec124d2da56c214f7"},
- {file = "pymongo-4.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:262a4073d2ee0654f0314ef4d9aab1d8c13dc8dae5c102312e152c02bfa7bdb7"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:022c91e2a41eefbcddc844c534520a13c6f613666c37b9fb9ed039eff47bd2e4"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a0d326c3ba989091026fbc4827638dc169abdbb0c0bbe593716921543f530af6"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5a1e5b931bf729b2eacd720a0e40201c2d5ed0e2bada60863f19b069bb5016c4"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:54d0b8b6f2548e15b09232827d9ba8e03a599c9a30534f7f2c7bae79df2d1f91"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e426e213ab07a73f8759ab8d69e87d05d7a60b3ecbf7673965948dcf8ebc1c9f"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:53831effe4dc0243231a944dfbd87896e42b1cf081776930de5cc74371405e3b"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:977c34b5b0b50bd169fbca1a4dd06fbfdfd8ac47734fdc3473532c10098e16ce"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab52db4d3aa3b73bcf920fb375dbea63bf0df0cb4bdb38c5a0a69e16568cc21"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bb935789276422d8875f051837356edfccdb886e673444d91e4941a8142bd48"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d45243ff4800320c842c45e01c91037e281840e8c6ed2949ed82a70f55c0e6a"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d6d2b7e14bb6bc052f6cba0c1cf4d47a2b49c56ea1ed0f960a02bc9afaefb2"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85b92b3828b2c923ed448f820c147ee51fa4566e35c9bf88415586eb0192ced2"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f345380f6d6d6d1dc6db9fa5c8480c439ea79553b71a2cbe3030a1f20676595"},
- {file = "pymongo-4.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dcc64747b628a96bcfc6405c42acae3762c85d8ae8c1ce18834b8151cad7486"},
- {file = "pymongo-4.4.1-cp38-cp38-win32.whl", hash = "sha256:ebe1683ec85d8bca389183d01ecf4640c797d6f22e6dac3453a6c492920d5ec3"},
- {file = "pymongo-4.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:58c492e28057838792bed67875f982ffbd3c9ceb67341cc03811859fddb8efbf"},
- {file = "pymongo-4.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aed21b3142311ad139629c4e101b54f25447ec40d6f42c72ad5c1a6f4f851f3a"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98764ae13de0ab80ba824ca0b84177006dec51f48dfb7c944d8fa78ab645c67f"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7b7127bb35f10d974ec1bd5573389e99054c558b821c9f23bb8ff94e7ae6e612"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:48409bac0f6a62825c306c9a124698df920afdc396132908a8e88b466925a248"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:55b6ebeeabe32a9d2e38eeb90f07c020cb91098b34b5fca42ff3991cb6e6e621"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:4e6a70c9d437b043fb07eef1796060f476359e5b7d8e23baa49f1a70379d6543"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:0bdbbcc1ef3a56347630c57eda5cd9536bdbdb82754b3108c66cbc51b5233dfb"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:04ec1c5451ad358fdbff28ddc6e8a3d1b5f62178d38cd08007a251bc3f59445a"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a7739bcebdbeb5648edb15af00fd38f2ab5de20851a1341d229494a638284cc"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02dba4ea2a6f22de4b50864d3957a0110b75d3eeb40aeab0b0ff64bcb5a063e6"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:884a35c0740744a48f67210692841581ab83a4608d3a031e7125022989ef65f8"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2aab6d1cff00d68212eca75d2260980202b14038d9298fed7d5c455fe3285c7c"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae1f85223193f249320f695eec4242cdcc311357f5f5064c2e72cfd18017e8ee"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b25d2ccdb2901655cc56c0fc978c5ddb35029c46bfd30d182d0e23fffd55b14b"},
- {file = "pymongo-4.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:334d41649f157c56a47fb289bae3b647a867c1a74f5f3a8a371fb361580bd9d3"},
- {file = "pymongo-4.4.1-cp39-cp39-win32.whl", hash = "sha256:c409e5888a94a3ff99783fffd9477128ffab8416e3f8b2c633993eecdcd5c267"},
- {file = "pymongo-4.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3681caf37edbe05f72f0d351e4a6cb5874ec7ab5eeb99df3a277dbf110093739"},
- {file = "pymongo-4.4.1.tar.gz", hash = "sha256:a4df87dbbd03ac6372d24f2a8054b4dc33de497d5227b50ec649f436ad574284"},
+ {file = "pymongo-4.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d4fa1b01fa7e5b7bb8d312e3542e211b320eb7a4e3d8dc884327039d93cb9e0"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:dfcd2b9f510411de615ccedd47462dae80e82fdc09fe9ab0f0f32f11cf57eeb5"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:3e33064f1984db412b34d51496f4ea785a9cff621c67de58e09fb28da6468a52"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:33faa786cc907de63f745f587e9879429b46033d7d97a7b84b37f4f8f47b9b32"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:76a262c41c1a7cbb84a3b11976578a7eb8e788c4b7bfbd15c005fb6ca88e6e50"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:0f4b125b46fe377984fbaecf2af40ed48b05a4b7676a2ff98999f2016d66b3ec"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:40d5f6e853ece9bfc01e9129b228df446f49316a4252bb1fbfae5c3c9dedebad"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:152259f0f1a60f560323aacf463a3642a65a25557683f49cfa08c8f1ecb2395a"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d64878d1659d2a5bdfd0f0a4d79bafe68653c573681495e424ab40d7b6d6d41"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1bb3a62395ffe835dbef3a1cbff48fbcce709c78bd1f52e896aee990928432b"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe48f50fb6348511a3268a893bfd4ab5f263f5ac220782449d03cd05964d1ae7"},
+ {file = "pymongo-4.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7591a3beea6a9a4fa3080d27d193b41f631130e3ffa76b88c9ccea123f26dc59"},
+ {file = "pymongo-4.5.0-cp310-cp310-win32.whl", hash = "sha256:3a7166d57dc74d679caa7743b8ecf7dc3a1235a9fd178654dddb2b2a627ae229"},
+ {file = "pymongo-4.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:21b953da14549ff62ea4ae20889c71564328958cbdf880c64a92a48dda4c9c53"},
+ {file = "pymongo-4.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ead4f19d0257a756b21ac2e0e85a37a7245ddec36d3b6008d5bfe416525967dc"},
+ {file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aff6279e405dc953eeb540ab061e72c03cf38119613fce183a8e94f31be608f"},
+ {file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4c8d6aa91d3e35016847cbe8d73106e3d1c9a4e6578d38e2c346bfe8edb3ca"},
+ {file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08819da7864f9b8d4a95729b2bea5fffed08b63d3b9c15b4fea47de655766cf5"},
+ {file = "pymongo-4.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a253b765b7cbc4209f1d8ee16c7287c4268d3243070bf72d7eec5aa9dfe2a2c2"},
+ {file = "pymongo-4.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8027c9063579083746147cf401a7072a9fb6829678076cd3deff28bb0e0f50c8"},
+ {file = "pymongo-4.5.0-cp311-cp311-win32.whl", hash = "sha256:9d2346b00af524757576cc2406414562cced1d4349c92166a0ee377a2a483a80"},
+ {file = "pymongo-4.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c3c3525ea8658ee1192cdddf5faf99b07ebe1eeaa61bf32821126df6d1b8072b"},
+ {file = "pymongo-4.5.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e5a27f348909235a106a3903fc8e70f573d89b41d723a500869c6569a391cff7"},
+ {file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9a9a39b7cac81dca79fca8c2a6479ef4c7b1aab95fad7544cc0e8fd943595a2"},
+ {file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:496c9cbcb4951183d4503a9d7d2c1e3694aab1304262f831d5e1917e60386036"},
+ {file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23cc6d7eb009c688d70da186b8f362d61d5dd1a2c14a45b890bd1e91e9c451f2"},
+ {file = "pymongo-4.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fff7d17d30b2cd45afd654b3fc117755c5d84506ed25fda386494e4e0a3416e1"},
+ {file = "pymongo-4.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6422b6763b016f2ef2beedded0e546d6aa6ba87910f9244d86e0ac7690f75c96"},
+ {file = "pymongo-4.5.0-cp312-cp312-win32.whl", hash = "sha256:77cfff95c1fafd09e940b3fdcb7b65f11442662fad611d0e69b4dd5d17a81c60"},
+ {file = "pymongo-4.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e57d859b972c75ee44ea2ef4758f12821243e99de814030f69a3decb2aa86807"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2b0176f9233a5927084c79ff80b51bd70bfd57e4f3d564f50f80238e797f0c8a"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89b3f2da57a27913d15d2a07d58482f33d0a5b28abd20b8e643ab4d625e36257"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5caee7bd08c3d36ec54617832b44985bd70c4cbd77c5b313de6f7fce0bb34f93"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1d40ad09d9f5e719bc6f729cc6b17f31c0b055029719406bd31dde2f72fca7e7"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:076afa0a4a96ca9f77fec0e4a0d241200b3b3a1766f8d7be9a905ecf59a7416b"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:3fa3648e4f1e63ddfe53563ee111079ea3ab35c3b09cd25bc22dadc8269a495f"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:44ee985194c426ddf781fa784f31ffa29cb59657b2dba09250a4245431847d73"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b33c17d9e694b66d7e96977e9e56df19d662031483efe121a24772a44ccbbc7e"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d79ae3bb1ff041c0db56f138c88ce1dfb0209f3546d8d6e7c3f74944ecd2439"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67225f05f6ea27c8dc57f3fa6397c96d09c42af69d46629f71e82e66d33fa4f"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41771b22dd2822540f79a877c391283d4e6368125999a5ec8beee1ce566f3f82"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a1f26bc1f5ce774d99725773901820dfdfd24e875028da4a0252a5b48dcab5c"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3236cf89d69679eaeb9119c840f5c7eb388a2110b57af6bb6baf01a1da387c18"},
+ {file = "pymongo-4.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e1f61355c821e870fb4c17cdb318669cfbcf245a291ce5053b41140870c3e5cc"},
+ {file = "pymongo-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:49dce6957598975d8b8d506329d2a3a6c4aee911fa4bbcf5e52ffc6897122950"},
+ {file = "pymongo-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2227a08b091bd41df5aadee0a5037673f691e2aa000e1968b1ea2342afc6880"},
+ {file = "pymongo-4.5.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:435228d3c16a375274ac8ab9c4f9aef40c5e57ddb8296e20ecec9e2461da1017"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8e559116e4128630ad3b7e788e2e5da81cbc2344dee246af44471fa650486a70"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:840eaf30ccac122df260b6005f9dfae4ac287c498ee91e3e90c56781614ca238"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b4fe46b58010115514b842c669a0ed9b6a342017b15905653a5b1724ab80917f"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:a8127437ebc196a6f5e8fddd746bd0903a400dc6b5ae35df672dd1ccc7170a2a"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:2988ef5e6b360b3ff1c6d55c53515499de5f48df31afd9f785d788cdacfbe2d3"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e249190b018d63c901678053b4a43e797ca78b93fb6d17633e3567d4b3ec6107"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1240edc1a448d4ada4bf1a0e55550b6292420915292408e59159fd8bbdaf8f63"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6d2a56fc2354bb6378f3634402eec788a8f3facf0b3e7d468db5f2b5a78d763"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a0aade2b11dc0c326ccd429ee4134d2d47459ff68d449c6d7e01e74651bd255"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74c0da07c04d0781490b2915e7514b1adb265ef22af039a947988c331ee7455b"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3754acbd7efc7f1b529039fcffc092a15e1cf045e31f22f6c9c5950c613ec4d"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:631492573a1bef2f74f9ac0f9d84e0ce422c251644cd81207530af4aa2ee1980"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e2654d1278384cff75952682d17c718ecc1ad1d6227bb0068fd826ba47d426a5"},
+ {file = "pymongo-4.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:168172ef7856e20ec024fe2a746bfa895c88b32720138e6438fd765ebd2b62dd"},
+ {file = "pymongo-4.5.0-cp38-cp38-win32.whl", hash = "sha256:b25f7bea162b3dbec6d33c522097ef81df7c19a9300722fa6853f5b495aecb77"},
+ {file = "pymongo-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:b520aafc6cb148bac09ccf532f52cbd31d83acf4d3e5070d84efe3c019a1adbf"},
+ {file = "pymongo-4.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8543253adfaa0b802bfa88386db1009c6ebb7d5684d093ee4edc725007553d21"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:bc5d8c3647b8ae28e4312f1492b8f29deebd31479cd3abaa989090fb1d66db83"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:505f8519c4c782a61d94a17b0da50be639ec462128fbd10ab0a34889218fdee3"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:53f2dda54d76a98b43a410498bd12f6034b2a14b6844ca08513733b2b20b7ad8"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:9c04b9560872fa9a91251030c488e0a73bce9321a70f991f830c72b3f8115d0d"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:58a63a26a1e3dc481dd3a18d6d9f8bd1d576cd1ffe0d479ba7dd38b0aeb20066"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:f076b779aa3dc179aa3ed861be063a313ed4e48ae9f6a8370a9b1295d4502111"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:1b1d7d9aabd8629a31d63cd106d56cca0e6420f38e50563278b520f385c0d86e"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37df8f6006286a5896d1cbc3efb8471ced42e3568d38e6cb00857277047b0d63"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56320c401f544d762fc35766936178fbceb1d9261cd7b24fbfbc8fb6f67aa8a5"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbd705d5f3c3d1ff2d169e418bb789ff07ab3c70d567cc6ba6b72b04b9143481"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80a167081c75cf66b32f30e2f1eaee9365af935a86dbd76788169911bed9b5d5"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c42748ccc451dfcd9cef6c5447a7ab727351fd9747ad431db5ebb18a9b78a4d"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf62da7a4cdec9a4b2981fcbd5e08053edffccf20e845c0b6ec1e77eb7fab61d"},
+ {file = "pymongo-4.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b5bbb87fa0511bd313d9a2c90294c88db837667c2bda2ea3fa7a35b59fd93b1f"},
+ {file = "pymongo-4.5.0-cp39-cp39-win32.whl", hash = "sha256:465fd5b040206f8bce7016b01d7e7f79d2fcd7c2b8e41791be9632a9df1b4999"},
+ {file = "pymongo-4.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:63d8019eee119df308a075b8a7bdb06d4720bf791e2b73d5ab0e7473c115d79c"},
+ {file = "pymongo-4.5.0.tar.gz", hash = "sha256:681f252e43b3ef054ca9161635f81b730f4d8cadd28b3f2b2004f5a72f853982"},
]
[package.dependencies]
@@ -1115,21 +1547,22 @@ dnspython = ">=1.16.0,<3.0.0"
[package.extras]
aws = ["pymongo-auth-aws (<2.0.0)"]
-encryption = ["pymongo-auth-aws (<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"]
-gssapi = ["pykerberos"]
-ocsp = ["certifi", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
+encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"]
+gssapi = ["pykerberos", "winkerberos (>=0.5.0)"]
+ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
snappy = ["python-snappy"]
zstd = ["zstandard"]
[[package]]
name = "pytest"
-version = "7.4.0"
+version = "7.4.2"
description = "pytest: simple powerful testing with Python"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
- {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
+ {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
+ {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
]
[package.dependencies]
@@ -1143,10 +1576,75 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+[[package]]
+name = "pytest-asyncio"
+version = "0.21.1"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
+ {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+
+[[package]]
+name = "pytest-mock"
+version = "3.12.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "main"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
+ {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
+]
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-http-client"
+version = "3.3.7"
+description = "HTTP REST client, simplified for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "python_http_client-3.3.7-py3-none-any.whl", hash = "sha256:ad371d2bbedc6ea15c26179c6222a78bc9308d272435ddf1d5c84f068f249a36"},
+ {file = "python_http_client-3.3.7.tar.gz", hash = "sha256:bf841ee45262747e00dec7ee9971dfb8c7d83083f5713596488d67739170cea0"},
+]
+
[[package]]
name = "python-multipart"
version = "0.0.6"
description = "A streaming multipart parser for Python"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1157,10 +1655,23 @@ files = [
[package.extras]
dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"]
+[[package]]
+name = "pytz"
+version = "2023.3.post1"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
+ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
+]
+
[[package]]
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
+category = "main"
optional = false
python-versions = "*"
files = [
@@ -1184,6 +1695,7 @@ files = [
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -1233,6 +1745,7 @@ files = [
name = "redis"
version = "4.6.0"
description = "Python client for Redis database and key-value store"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1251,6 +1764,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1268,10 +1782,136 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "requests-file"
+version = "1.5.1"
+description = "File transport adapter for Requests"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"},
+ {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"},
+]
+
+[package.dependencies]
+requests = ">=1.0.0"
+six = "*"
+
+[[package]]
+name = "restrictedpython"
+version = "6.2"
+description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment."
+category = "main"
+optional = false
+python-versions = ">=3.6, <3.12"
+files = [
+ {file = "RestrictedPython-6.2-py3-none-any.whl", hash = "sha256:7c2ffa4904300d67732f841d8a975dcdc53eba4c1cdc9d84b97684ef12304a3d"},
+ {file = "RestrictedPython-6.2.tar.gz", hash = "sha256:db73eb7e3b39650f0d21d10cc8dda9c0e2986e621c94b0c5de32fb0dee3a08af"},
+]
+
+[package.extras]
+docs = ["Sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-mock"]
+
+[[package]]
+name = "s3transfer"
+version = "0.7.0"
+description = "An Amazon S3 Transfer Manager"
+category = "main"
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:10d6923c6359175f264811ef4bf6161a3156ce8e350e705396a7557d6293c33a"},
+ {file = "s3transfer-0.7.0.tar.gz", hash = "sha256:fd3889a66f5fe17299fe75b82eae6cf722554edca744ca5d5fe308b104883d2e"},
+]
+
+[package.dependencies]
+botocore = ">=1.12.36,<2.0a.0"
+
+[package.extras]
+crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"]
+
+[[package]]
+name = "sendgrid"
+version = "6.10.0"
+description = "Twilio SendGrid library for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "sendgrid-6.10.0-py3-none-any.whl", hash = "sha256:522b30fc98306496208c5d8bdd5642cd6a2fd65cad487475f57f9098ce880604"},
+ {file = "sendgrid-6.10.0.tar.gz", hash = "sha256:9b15050c6f8826ee576f76a786efb15d956639f485478cbddd79ed69e8350ab8"},
+]
+
+[package.dependencies]
+python-http-client = ">=3.2.1"
+starkbank-ecdsa = ">=2.0.1"
+
+[[package]]
+name = "sentry-sdk"
+version = "1.34.0"
+description = "Python client for Sentry (https://sentry.io)"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "sentry-sdk-1.34.0.tar.gz", hash = "sha256:e5d0d2b25931d88fa10986da59d941ac6037f742ab6ff2fce4143a27981d60c3"},
+ {file = "sentry_sdk-1.34.0-py2.py3-none-any.whl", hash = "sha256:76dd087f38062ac6c1e30ed6feb533ee0037ff9e709974802db7b5dbf2e5db21"},
+]
+
+[package.dependencies]
+certifi = "*"
+fastapi = {version = ">=0.79.0", optional = true, markers = "extra == \"fastapi\""}
+urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.5)"]
+arq = ["arq (>=0.23)"]
+asyncpg = ["asyncpg (>=0.23)"]
+beam = ["apache-beam (>=2.12)"]
+bottle = ["bottle (>=0.12.13)"]
+celery = ["celery (>=3)"]
+chalice = ["chalice (>=1.16.0)"]
+clickhouse-driver = ["clickhouse-driver (>=0.2.0)"]
+django = ["django (>=1.8)"]
+falcon = ["falcon (>=1.4)"]
+fastapi = ["fastapi (>=0.79.0)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
+grpcio = ["grpcio (>=1.21.1)"]
+httpx = ["httpx (>=0.16.0)"]
+huey = ["huey (>=2)"]
+loguru = ["loguru (>=0.5)"]
+opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
+opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"]
+pure-eval = ["asttokens", "executing", "pure-eval"]
+pymongo = ["pymongo (>=3.1)"]
+pyspark = ["pyspark (>=2.4.4)"]
+quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
+rq = ["rq (>=0.6)"]
+sanic = ["sanic (>=0.8)"]
+sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
+starlite = ["starlite (>=1.48)"]
+tornado = ["tornado (>=5)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1283,6 +1923,7 @@ files = [
name = "sqlalchemy"
version = "1.4.41"
description = "Database Abstraction Library"
+category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
@@ -1330,7 +1971,7 @@ files = [
]
[package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"}
+greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""}
[package.extras]
aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
@@ -1357,6 +1998,7 @@ sqlcipher = ["sqlcipher3-binary"]
name = "sqlalchemy2-stubs"
version = "0.0.2a35"
description = "Typing Stubs for SQLAlchemy 1.4"
+category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -1371,6 +2013,7 @@ typing-extensions = ">=3.7.4"
name = "sqlmodel"
version = "0.0.8"
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
+category = "main"
optional = false
python-versions = ">=3.6.1,<4.0.0"
files = [
@@ -1383,10 +2026,22 @@ pydantic = ">=1.8.2,<2.0.0"
SQLAlchemy = ">=1.4.17,<=1.4.41"
sqlalchemy2-stubs = "*"
+[[package]]
+name = "starkbank-ecdsa"
+version = "2.2.0"
+description = "A lightweight and fast pure python ECDSA library"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "starkbank-ecdsa-2.2.0.tar.gz", hash = "sha256:9399c3371b899d4a235b68a1ed7919d202fbf024bd2c863ae8ebdad343c2a63a"},
+]
+
[[package]]
name = "starlette"
version = "0.27.0"
description = "The little ASGI library that shines."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1401,24 +2056,75 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""
[package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
+[[package]]
+name = "supertokens-python"
+version = "0.15.3"
+description = "SuperTokens SDK for Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "supertokens_python-0.15.3-py3-none-any.whl", hash = "sha256:153173fd93b12a023f96f547208e27692fd08471bfc92dc94c6e23be5f98e629"},
+ {file = "supertokens_python-0.15.3.tar.gz", hash = "sha256:233e774f09e7470af025a22fa124c09bcddf4b59aeab86d87211e5f3afb8dac3"},
+]
+
+[package.dependencies]
+aiosmtplib = "1.1.6"
+asgiref = ">=3.4.1,<4"
+Deprecated = "1.2.13"
+httpx = ">=0.15.0,<0.25.0"
+phonenumbers = "8.12.48"
+pkce = "1.0.3"
+pycryptodome = ">=3.10.0,<3.11.0"
+PyJWT = {version = ">=2.6.0,<3.0.0", extras = ["crypto"]}
+tldextract = "3.1.0"
+twilio = "7.9.1"
+typing-extensions = ">=4.1.1,<5.0.0"
+
+[package.extras]
+django = ["django (>=3)", "django-cors-headers (==3.11.0)", "django-stubs (==1.9.0)", "python-dotenv (==0.19.2)", "uvicorn (==0.18.2)"]
+django2x = ["django (>=2,<3)", "django-cors-headers (==3.11.0)", "django-stubs (==1.9.0)", "gunicorn (==20.1.0)", "python-dotenv (==0.19.2)"]
+fastapi = ["Fastapi", "python-dotenv (==0.19.2)", "respx (==0.19.2)", "uvicorn (==0.18.2)"]
+flask = ["Flask", "flask-cors", "python-dotenv (==0.19.2)"]
+
[[package]]
name = "tenacity"
-version = "8.2.2"
+version = "8.2.3"
description = "Retry code until it succeeds"
+category = "main"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {file = "tenacity-8.2.2-py3-none-any.whl", hash = "sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0"},
- {file = "tenacity-8.2.2.tar.gz", hash = "sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0"},
+ {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"},
+ {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"},
]
[package.extras]
doc = ["reno", "sphinx", "tornado (>=4.5)"]
+[[package]]
+name = "tldextract"
+version = "3.1.0"
+description = "Accurately separate the TLD from the registered domain and subdomains of a URL, using the Public Suffix List. By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "tldextract-3.1.0-py2.py3-none-any.whl", hash = "sha256:e57f22b6d00a28c21673d2048112f1bdcb6a14d4711568305f6bb96cf5bb53a1"},
+ {file = "tldextract-3.1.0.tar.gz", hash = "sha256:cfae9bc8bda37c3e8c7c8639711ad20e95dc85b207a256b60b0b23d7ff5540ea"},
+]
+
+[package.dependencies]
+filelock = ">=3.0.8"
+idna = "*"
+requests = ">=2.1.0"
+requests-file = ">=1.4"
+
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
+category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -1430,6 +2136,7 @@ files = [
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1441,6 +2148,7 @@ files = [
name = "tqdm"
version = "4.66.1"
description = "Fast, Extensible Progress Meter"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1457,21 +2165,40 @@ notebook = ["ipywidgets (>=6)"]
slack = ["slack-sdk"]
telegram = ["requests"]
+[[package]]
+name = "twilio"
+version = "7.9.1"
+description = "Twilio API client and TwiML generator"
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "twilio-7.9.1-py2.py3-none-any.whl", hash = "sha256:9e9b7706b606474e3d6d208df0c40db9a0a387235e01071e8f75501af0949fb4"},
+ {file = "twilio-7.9.1.tar.gz", hash = "sha256:4b9c04dc45736cbfa6449351b9fb8a23b5738afc6bc79b1262256583983432c5"},
+]
+
+[package.dependencies]
+PyJWT = ">=2.0.0,<3.0.0"
+pytz = "*"
+requests = ">=2.0.0"
+
[[package]]
name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
+version = "4.8.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
- {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
+ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]]
name = "typing-inspect"
version = "0.9.0"
description = "Runtime inspection utilities for typing module."
+category = "main"
optional = false
python-versions = "*"
files = [
@@ -1485,13 +2212,31 @@ typing-extensions = ">=3.7.4"
[[package]]
name = "urllib3"
-version = "2.0.4"
+version = "1.26.18"
description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+ {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"},
+ {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"},
+]
+
+[package.extras]
+brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "urllib3"
+version = "2.0.7"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
- {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
- {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
+ {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"},
+ {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"},
]
[package.extras]
@@ -1504,6 +2249,7 @@ zstd = ["zstandard (>=0.18.0)"]
name = "uvicorn"
version = "0.22.0"
description = "The lightning-fast ASGI server."
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1520,24 +2266,111 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[[package]]
name = "websocket-client"
-version = "1.6.1"
+version = "1.6.4"
description = "WebSocket client for Python with low level API options"
+category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"},
- {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"},
+ {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"},
+ {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"},
]
[package.extras]
-docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
+docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"]
optional = ["python-socks", "wsaccel"]
test = ["websockets"]
+[[package]]
+name = "wrapt"
+version = "1.15.0"
+description = "Module for decorators, wrappers and monkey patching."
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
+ {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
+ {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
+ {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
+ {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
+ {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
+ {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
+ {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
+ {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
+ {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
+ {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
+ {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
+ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+]
+
[[package]]
name = "yarl"
version = "1.9.2"
description = "Yet another URL library"
+category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1624,4 +2457,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "b48b0b3ac88bfaae97eba6b9ecbcc5af209d09af1063d26f9a85406d7e697d0d"
+content-hash = "75932f4c0802e7c85d7d741e634213229440e26fc77e5127b0af1da4f3e55aad"
diff --git a/agenta-backend/pyproject.toml b/agenta-backend/pyproject.toml
index b1e4c128d7..dec889d62c 100644
--- a/agenta-backend/pyproject.toml
+++ b/agenta-backend/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "agenta_backend"
-version = "0.1.0"
+version = "0.2.2"
description = ""
authors = ["Mahmoud Mabrouk "]
readme = "README.md"
@@ -21,11 +21,20 @@ redis = "^4.6.0"
aiodocker = "^0.21.0"
openai = "^0.27.8"
langchain = "^0.0.256"
-
+odmantic = "^0.9.2"
+supertokens-python = "^0.15.1"
+sendgrid = "^6.10.0"
+restrictedpython = { version = "^6.2", python = ">=3.9,<3.12" }
+pytest-mock = "^3.11.1"
+boto3 = "^1.28.63"
+asyncer = "^0.0.2"
+anyio = "==3.7.1"
+sentry-sdk = {extras = ["fastapi"], version = "^1.34.0"}
[tool.poetry.group.dev.dependencies]
pytest = "^7.3.1"
httpx = "^0.24.0"
+pytest-asyncio = "^0.21.1"
[build-system]
requires = ["poetry-core"]
diff --git a/agenta-backend/sleep.py b/agenta-backend/sleep.py
deleted file mode 100644
index 014a036588..0000000000
--- a/agenta-backend/sleep.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from time import sleep
-
-while True:
- sleep(1000000000)
diff --git a/agenta-backend/tests/config.toml b/agenta-backend/tests/config.toml
deleted file mode 100644
index 72ee0ffbb7..0000000000
--- a/agenta-backend/tests/config.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-docker_registry_url="registry:5000"
-database_url="localhost:5432"
-registry="agenta-server"
\ No newline at end of file
diff --git a/agenta-backend/tests/conftest.py b/agenta-backend/tests/conftest.py
deleted file mode 100644
index c66872f036..0000000000
--- a/agenta-backend/tests/conftest.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import pytest
-from fastapi.testclient import TestClient
-from agenta_backend.main import app
-
-
-@pytest.fixture(scope="session", autouse=True)
-def test_app():
- client = TestClient(app)
- yield client # provide the test client to the tests
- # teardown code goes here
diff --git a/agenta-backend/tests/manual_tests.py b/agenta-backend/tests/manual_tests.py
deleted file mode 100644
index d953a857b0..0000000000
--- a/agenta-backend/tests/manual_tests.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import os
-import docker
-from agenta_backend.config import settings
-
-from agenta_backend.services import app_manager
-from agenta_backend.services import db_manager
-from agenta_backend.models.api.api_models import AppVariant, Image, URI
-
-db_manager.print_all()
-app_manager.remove_app("baby_name_generator")
-# app = AppVariant(app_name="baby_name_generator", variant_name="v0")
-# app_manager.remove_app_variant(app)
-# app = AppVariant(app_name="baby_name_generator", variant_name="v0.2")
-# app_manager.remove_app_variant(app)
diff --git a/agenta-backend/tests/test_db_manager.py b/agenta-backend/tests/test_db_manager.py
deleted file mode 100644
index fc10b1023a..0000000000
--- a/agenta-backend/tests/test_db_manager.py
+++ /dev/null
@@ -1,416 +0,0 @@
-from random import choice
-from string import ascii_letters
-
-import pytest
-from agenta_backend.models.api.api_models import App, AppVariant, Image
-from agenta_backend.services.db_manager import (
- add_variant_based_on_image,
- engine,
- get_image,
- get_session,
- list_apps,
- list_app_variants,
- remove_app_variant,
- add_variant_based_on_previous,
- print_all,
-)
-from sqlmodel import Session
-from time import sleep
-
-
-@pytest.fixture(autouse=True)
-def cleanup():
- """Fixture that is automatically used before each test."""
- # Setup: clean up the database
-
- with Session(engine) as session:
- # adjust with your table name
- session.execute("DELETE FROM appvariantdb")
- session.execute("DELETE FROM imagedb") # adjust with your table name
- session.commit()
- yield
-
-
-def test_get_session():
- assert get_session() is not None
-
-
-def test_list():
- print(list_app_variants())
- assert list_app_variants() == []
-
-
-def random_string(length=10):
- return "".join(choice(ascii_letters) for _ in range(length))
-
-
-@pytest.fixture
-def app_variant():
- return AppVariant(app_name=random_string(), variant_name=random_string())
-
-
-@pytest.fixture
-def app_variant2():
- return AppVariant(app_name=random_string(), variant_name=random_string())
-
-
-@pytest.fixture
-def image() -> Image:
- return Image(docker_id=random_string(), tags=random_string())
-
-
-def test_add_and_check_exists(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- app_variants = list_app_variants()
- assert len(app_variants) == 1
- assert app_variants[0].app_name == app_variant.app_name
- assert app_variants[0].variant_name == app_variant.variant_name
-
-
-def test_add_and_remove(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- remove_app_variant(app_variant)
- app_variants = list_app_variants()
- assert len(app_variants) == 0
-
-
-def test_listing_when_empty():
- app_variants = list_app_variants()
- assert len(app_variants) == 0
-
-
-def test_add_variant_based_on_image_with_empty_name(image):
- with pytest.raises(ValueError):
- add_variant_based_on_image(
- AppVariant(app_name="", variant_name=random_string()), image
- )
-
-
-def test_add_same_app_variant_twice(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- # Assumes your function raises ValueError for duplicate adds
- with pytest.raises(ValueError):
- add_variant_based_on_image(app_variant, image)
-
-
-def test_remove_non_existent_app_variant():
- non_existent_app_variant = AppVariant(
- app_name=random_string(), variant_name=random_string()
- )
- with pytest.raises(ValueError):
- remove_app_variant(non_existent_app_variant) # Should not raise an error
- assert len(list_app_variants()) == 0
-
-
-def test_add_multiple_versions_same_app(image):
- app_name = random_string()
- for _ in range(2): # Add 2 versions of the same app
- app_variant = AppVariant(app_name=app_name, variant_name=random_string())
- add_variant_based_on_image(app_variant, image)
- app_variants = list_app_variants()
- assert len(app_variants) == 2
- for version in app_variants:
- assert version.app_name == app_name
-
-
-def test_add_remove_different_order(app_variant, app_variant2, image):
- add_variant_based_on_image(app_variant, image)
- add_variant_based_on_image(app_variant2, image)
- remove_app_variant(app_variant)
- app_variants = list_app_variants()
- assert len(app_variants) == 1
- assert app_variants[0].app_name == app_variant2.app_name
- assert app_variants[0].variant_name == app_variant2.variant_name
-
-
-def test_add_variant_based_on_image_with_image(app_variant, image):
- """Adds variant based on image using an image object
-
- Arguments:
- app_variant -- _description_
- image -- _description_
- """
- add_variant_based_on_image(app_variant, image)
- app_variants = list_app_variants()
- assert len(app_variants) == 1
- assert app_variants[0].app_name == app_variant.app_name
- assert app_variants[0].variant_name == app_variant.variant_name
- image_ = get_image(
- AppVariant(app_name=app_variant.app_name, variant_name=app_variant.variant_name)
- )
- assert image.docker_id == image_.docker_id
- assert image.tags == image_.tags
-
-
-def test_filter_by_app_name(image: Image):
- """Adds some app variants with two different apps and checks that list_app_variants
- with the given app_name returns only the app variants with that app_name
-
- Arguments:
- image -- _description_
- """
- # Assuming you have a setUp function that clears the database before each test
- app_name = "test_app"
- other_app_name = "other_app"
-
- # Generate some random app variants
- for _ in range(5):
- variant_name = "".join(choice(ascii_letters) for _ in range(10))
- add_variant_based_on_image(
- AppVariant(app_name=app_name, variant_name=variant_name), image
- )
-
- # Generate some random app variants with a different app name
- for _ in range(3):
- variant_name = "".join(choice(ascii_letters) for _ in range(10))
- add_variant_based_on_image(
- AppVariant(app_name=other_app_name, variant_name=variant_name), image
- )
-
- # Check that list_app_variants with the given app_name returns only the app variants with that app_name
- app_variants = list_app_variants(app_name)
- assert len(app_variants) == 5
- for app_variant in app_variants:
- assert app_variant.app_name == app_name
-
-
-def test_list_apps(image):
- # Assuming you have a setUp function that clears the database before each test
- app_name1 = "test_app"
- app_name2 = "other_app"
-
- # Generate some random app variants
- for _ in range(5):
- variant_name = "".join(choice(ascii_letters) for _ in range(10))
- add_variant_based_on_image(
- AppVariant(app_name=app_name1, variant_name=variant_name), image
- )
-
- # Generate some random app variants with a different app name
- for _ in range(3):
- variant_name = "".join(choice(ascii_letters) for _ in range(10))
- add_variant_based_on_image(
- AppVariant(app_name=app_name2, variant_name=variant_name), image
- )
-
- # Check that list_apps returns all unique app names
- app_names = list_apps()
- assert len(app_names) == 2
- assert App(app_name=app_name1) in app_names
- assert App(app_name=app_name2) in app_names
-
-
-# Tests for variants with parameters
-
-
-def test_add_and_check_exists_based_on_previous(app_variant, image):
- add_variant_based_on_image(app_variant, image)
-
- app_variants = list_app_variants()
- new_variant = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value1", "param2": 10},
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant.variant_name,
- parameters=new_variant.parameters,
- )
- app_variants = list_app_variants()
- assert len(app_variants) == 2
- assert app_variants[1].app_name == new_variant.app_name
- assert app_variants[1].variant_name == new_variant.variant_name
- assert app_variants[1].parameters == new_variant.parameters
-
-
-def test_add_two_variants_based_on_previous(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- new_variant1 = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value1", "param2": 10},
- )
- new_variant2 = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value3", "param2": 20},
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant1.variant_name,
- parameters=new_variant1.parameters,
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant2.variant_name,
- parameters=new_variant2.parameters,
- )
- app_variants = list_app_variants()
- assert len(app_variants) == 3
- for av in app_variants:
- if av.variant_name == new_variant1.variant_name:
- assert av.parameters == new_variant1.parameters
- elif av.variant_name == new_variant2.variant_name:
- assert av.parameters == new_variant2.parameters
-
-
-def test_add_and_remove_based_on_previous(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- new_variant = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value1", "param2": 10},
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant.variant_name,
- parameters=new_variant.parameters,
- )
- remove_app_variant(new_variant)
- app_variants = list_app_variants()
- assert len(app_variants) == 1
- assert app_variants[0].app_name == app_variant.app_name
- assert app_variants[0].variant_name == app_variant.variant_name
-
-
-def test_add_based_on_previous_without_parameters(app_variant, image):
- with pytest.raises(ValueError):
- add_variant_based_on_image(app_variant, image)
- new_variant = AppVariant(
- app_name=app_variant.app_name, variant_name=random_string()
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant.variant_name,
- parameters=None,
- )
-
-
-def test_add_based_on_previous_invalid_previous(app_variant, image):
- add_variant_based_on_image(app_variant, image)
- new_variant = AppVariant(
- app_name=random_string(),
- variant_name=random_string(),
- parameters={"param1": "value1", "param2": 10},
- )
- with pytest.raises(ValueError):
- add_variant_based_on_previous(
- previous_app_variant=new_variant,
- new_variant_name=new_variant.variant_name,
- parameters=new_variant.parameters,
- )
-
-
-def test_add_remove_chain_of_variants(app_variant, image):
- # Add original app variant
- add_variant_based_on_image(app_variant, image)
-
- # Add new variant based on original one
- new_variant1 = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value1", "param2": 10},
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant1.variant_name,
- parameters=new_variant1.parameters,
- )
-
- # Add another variant based on the previous one
- new_variant2 = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters={"param1": "value2", "param2": 20},
- )
- with pytest.raises(ValueError):
- add_variant_based_on_previous(
- previous_app_variant=new_variant1,
- new_variant_name=new_variant2.variant_name,
- parameters=new_variant2.parameters,
- )
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=new_variant2.variant_name,
- parameters=new_variant2.parameters,
- )
-
- assert get_image(new_variant1) is not None
- # Remove original variant
- remove_app_variant(app_variant)
-
- # Image should still exist as other variants are using it
- get_image(app_variant)
-
- assert get_image(new_variant1) is not None
-
- # Remove the new variant1
- remove_app_variant(new_variant1)
-
- # Image should still exist as new_variant2 is using it
- with pytest.raises(Exception):
- get_image(new_variant1)
- assert get_image(new_variant2) is not None
-
- # Remove the new variant2
- remove_app_variant(new_variant2)
-
- # Now the image should not exist as all variants using it have been removed
- with pytest.raises(Exception):
- get_image(new_variant2)
-
-
-def test_add_variant_based_on_previous(app_variant, app_variant2, image):
- add_variant_based_on_image(app_variant, image)
- parameters = {"key": "value"}
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=app_variant2.variant_name,
- parameters=parameters,
- )
- app_variants = list_app_variants()
- assert len(app_variants) == 2
- assert app_variants[1].app_name == app_variant.app_name
- assert app_variants[1].variant_name == app_variant2.variant_name
-
-
-def test_remove_app_variant_and_check_soft_deletion(app_variant, app_variant2, image):
- add_variant_based_on_image(app_variant, image)
- parameters = {"key": "value"}
- add_variant_based_on_previous(app_variant, app_variant2.variant_name, parameters)
- remove_app_variant(app_variant)
- app_variants = list_app_variants(show_soft_deleted=True)
- assert len(app_variants) == 2
- app_variants = list_app_variants()
- assert len(app_variants) == 1
-
-
-def test_add_variant_after_remove(app_variant, app_variant2, image):
- add_variant_based_on_image(app_variant, image)
- remove_app_variant(app_variant)
- parameters = {"key": "value"}
- with pytest.raises(ValueError):
- add_variant_based_on_previous(
- previous_app_variant=app_variant,
- new_variant_name=app_variant2.variant_name,
- parameters=parameters,
- )
-
-
-def test_add_variant_based_on_previous_with_soft_deleted_variant(
- app_variant, app_variant2, image
-):
- add_variant_based_on_image(app_variant, image)
- parameters = {"key": "value"}
- add_variant_based_on_previous(
- app_variant, app_variant2.variant_name + "2", parameters
- )
- remove_app_variant(app_variant)
-
- add_variant_based_on_previous(app_variant, app_variant2.variant_name, parameters)
- app_variants = list_app_variants()
- print_all()
- assert len(app_variants) == 2
- assert app_variants[1].app_name == app_variant.app_name
- assert app_variants[1].variant_name == app_variant2.variant_name
diff --git a/agenta-backend/tests/test_docker_utils.py b/agenta-backend/tests/test_docker_utils.py
deleted file mode 100644
index 122d7719f7..0000000000
--- a/agenta-backend/tests/test_docker_utils.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import pytest
-from unittest.mock import MagicMock, patch
-from agenta_backend.services.docker_utils import (
- start_container,
- stop_container,
- delete_container,
- list_images,
-)
-from agenta_backend.models.api.api_models import Image
-
-
-@pytest.fixture
-def mock_client():
- with patch("docker.from_env") as mock:
- yield mock()
-
-
-def test_start_container(mock_client):
- mock_image = MagicMock()
- mock_container = MagicMock()
- mock_client.images.get.return_value = mock_image
- mock_client.containers.run.return_value = mock_container
-
- container = start_container("test_image", "test_tag")
-
- mock_client.images.get.assert_called_once_with(
- "test_registry_url/test_image:test_tag"
- ) # replace with your actual registry url
- assert isinstance(container, Container)
-
-
-def test_stop_container(mock_client):
- mock_container = MagicMock()
- mock_client.containers.get.return_value = mock_container
-
- stop_container("test_id")
-
- mock_client.containers.get.assert_called_once_with("test_id")
- mock_container.stop.assert_called_once()
-
-
-def test_delete_container(mock_client):
- mock_container = MagicMock()
- mock_client.containers.get.return_value = mock_container
-
- delete_container("test_id")
-
- mock_client.containers.get.assert_called_once_with("test_id")
- mock_container.remove.assert_called_once()
-
-
-def test_list_images(mock_client):
- mock_image = MagicMock()
- mock_image.id = "test_id"
- # replace with your actual registry
- mock_image.tags = ["test_registry/test_tag"]
- mock_client.images.list.return_value = [mock_image]
-
- images = list_images()
-
- assert len(images) == 1
- assert isinstance(images[0], Image)
- assert images[0].docker_id == "test_id"
- # replace with your actual registry
- assert images[0].tags == "test_registry/test_tag"
diff --git a/agenta-backend/tests/test_router_app_variant.py b/agenta-backend/tests/test_router_app_variant.py
deleted file mode 100644
index c1dac99068..0000000000
--- a/agenta-backend/tests/test_router_app_variant.py
+++ /dev/null
@@ -1,160 +0,0 @@
-import io
-from random import choice
-from string import ascii_letters
-
-import docker
-import pytest
-from agenta_backend.main import app
-from agenta_backend.models.api.api_models import AppVariant, Image
-from agenta_backend.services.db_manager import (
- add_variant_based_on_image,
- engine,
- get_image,
- get_session,
- list_app_variants,
- remove_app_variant,
-)
-from fastapi.testclient import TestClient
-from sqlmodel import Session
-
-client = TestClient(app)
-
-
-@pytest.fixture(autouse=True)
-def cleanup():
- """Fixture that is automatically used before each test."""
- # Setup: clean up the database
-
- with Session(engine) as session:
- # adjust with your table name
- session.execute("DELETE FROM appvariantdb")
- session.execute("DELETE FROM imagedb") # adjust with your table name
- session.commit()
- yield
-
-
-@pytest.fixture(scope="session")
-def docker_client():
- return docker.from_env()
-
-
-def random_string(length=10):
- return "".join(choice(ascii_letters) for _ in range(length))
-
-
-@pytest.fixture
-def app_variant():
- return AppVariant(app_name=random_string(), variant_name=random_string())
-
-
-@pytest.fixture
-def app_variant2():
- return AppVariant(app_name=random_string(), variant_name=random_string())
-
-
-@pytest.fixture
-def image():
- return Image(docker_id=random_string(), tags=random_string())
-
-
-@pytest.fixture(scope="session")
-def docker_test_image(docker_client):
- # Create a simple Docker image using Python's official image
- # This Dockerfile will just create a new image based on python:3.9-slim
- dockerfile = """
- FROM python:3.9-slim
- """
-
- image, _ = docker_client.images.build(
- fileobj=io.BytesIO(dockerfile.encode("utf-8")), tag="agenta-server/test:latest"
- )
- return image
-
-
-@pytest.fixture
-def app_variant_parameters():
- return {"param1": 123, "param2": "abc"}
-
-
-def test_list_app_variant():
- response = client.get("/app_variant/list_variants/")
- assert response.status_code == 200
- assert response.json() == []
-
-
-def test_list_app_variant_after_manual_add(app_variant, image):
- # This is the function from db_manager.py
- add_variant_based_on_image(app_variant, image)
- response = client.get("/app_variant/list_variants/")
- assert response.status_code == 200
- assert len(response.json()) == 1
- result = AppVariant(**response.json()[0])
- assert result.app_name == app_variant.app_name
- assert result.variant_name == app_variant.variant_name
-
-
-def test_add_variant_from_image(app_variant, docker_test_image):
- image = Image(docker_id=docker_test_image.id, tags=docker_test_image.tags[0])
- response = client.post(
- "app_variant/add/from_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
- )
- assert response.status_code == 200
- response = client.get("/app_variant/list_variants/")
- assert response.status_code == 200
- assert len(response.json()) == 1
- result = AppVariant(**response.json()[0])
- assert result.app_name == app_variant.app_name
- assert result.variant_name == app_variant.variant_name
-
-
-def test_add_variant_with_wrong_image_tag(app_variant, image):
- response = client.post(
- "app_variant/add/from_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
- )
- assert response.status_code == 500
-
-
-def test_add_variant_not_in_docker_registry(app_variant, image):
- image.tags = "agenta-server/notexist:latest"
- response = client.post(
- "app_variant/add/from_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
- )
- assert response.status_code == 500
-
-
-def test_add_variant_from_template(
- app_variant, app_variant_parameters, docker_test_image
-):
- # First we add an initial variant from image
- image = Image(docker_id=docker_test_image.id, tags=docker_test_image.tags[0])
- response = client.post(
- "/app_variant/add/from_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
- )
- assert response.status_code == 200, response.content
-
- app_variant2 = AppVariant(
- app_name=app_variant.app_name,
- variant_name=random_string(),
- parameters=app_variant_parameters,
- )
- # Now we add a second variant from the template of the first variant
- response = client.post(
- "/app_variant/add/from_previous/",
- json={
- "previous_app_variant": app_variant.dict(),
- "new_variant_name": app_variant2.variant_name,
- "parameters": app_variant_parameters,
- },
- )
- assert response.status_code == 200, response.content
-
- # Verify that the new variant is listed
- response = client.get("/app_variant/list_variants/")
- assert response.status_code == 200, response.content
- assert len(response.json()) == 2
- result = [AppVariant(**r) for r in response.json()]
- assert app_variant2 in result
diff --git a/agenta-cli/README.md b/agenta-cli/README.md
index 51b5b253f1..4ed6b88633 100644
--- a/agenta-cli/README.md
+++ b/agenta-cli/README.md
@@ -1,5 +1,207 @@
-# Agenta SDK and CLI
+
+
+
+
+
+
+
+
+
+
+
+
+ Quickly iterate, debug, and evaluate your LLM apps
+ The open-source LLMOps platform for prompt-engineering, evaluation, and deployment of complex LLM apps.
+
+
+
+
+
+
+
+
+
+
+
+
-This directory contains the code source for the python SDK and CLI for Agenta AI.
-Please see the Readme.md in the main dir for installation and usage instructions.
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+
+
+---
+
+# ℹ️ About
+
+Building production-ready LLM-powered applications is currently very difficult. It involves countless iterations of prompt engineering, parameter tuning, and architectures.
+
+Agenta provides you with the tools to quickly do prompt engineering and 🧪 **experiment**, ⚖️ **evaluate**, and :rocket: **deploy** your LLM apps. All without imposing any restrictions on your choice of framework, library, or model.
+
+
+
+
+
+
+
+
+
+
+# Demo
+https://github.com/Agenta-AI/agenta/assets/57623556/99733147-2b78-4b95-852f-67475e4ce9ed
+
+# Quick Start
+
+
+
+
+# Features
+
+Playground 🪄
+ With just a few lines of code, define the parameters and prompts you wish to experiment with. You and your team can quickly experiment and test new variants on the web UI.
+
+https://github.com/Agenta-AI/agenta/assets/4510758/8b736d2b-7c61-414c-b534-d95efc69134c
+
+Version Evaluation 📊
+Define test sets, the evaluate manually or programmatically your different variants.
+
+
+
+
+API Deployment 🚀
+When you are ready, deploy your LLM applications as APIs in one click.
+
+
+
+## Why choose Agenta for building LLM-apps?
+
+- 🔨 **Build quickly**: You need to iterate many times on different architectures and prompts to bring apps to production. We streamline this process and allow you to do this in days instead of weeks.
+- 🏗️ **Build robust apps and reduce hallucination**: We provide you with the tools to systematically and easily evaluate your application to make sure you only serve robust apps to production
+- 👨💻 **Developer-centric**: We cater to complex LLM-apps and pipelines that require more than one simple prompt. We allow you to experiment and iterate on apps that have complex integration, business logic, and many prompts.
+- 🌐 **Solution-Agnostic**: You have the freedom to use any library and models, be it Langchain, llma_index, or a custom-written alternative.
+- 🔒 **Privacy-First**: We respect your privacy and do not proxy your data through third-party services. The platform and the data are hosted on your infrastructure.
+
+## How Agenta works:
+
+**1. Write your LLM-app code**
+
+Write the code using any framework, library, or model you want. Add the `agenta.post` decorator and put the inputs and parameters in the function call just like in this example:
+
+_Example simple application that generates baby names_
+
+```python
+import agenta as ag
+from langchain.chains import LLMChain
+from langchain.llms import OpenAI
+from langchain.prompts import PromptTemplate
+
+default_prompt = "Give me five cool names for a baby from {country} with this gender {gender}!!!!"
+
+
+@ag.post
+def generate(
+ country: str,
+ gender: str,
+ temperature: ag.FloatParam = 0.9,
+ prompt_template: ag.TextParam = default_prompt,
+) -> str:
+ llm = OpenAI(temperature=temperature)
+ prompt = PromptTemplate(
+ input_variables=["country", "gender"],
+ template=prompt_template,
+ )
+ chain = LLMChain(llm=llm, prompt=prompt)
+ output = chain.run(country=country, gender=gender)
+
+ return output
+```
+
+**2.Deploy your app using the Agenta CLI.**
+
+
+
+**3. Go to agenta at http://localhost**
+
+Now your team can 🔄 iterate, 🧪 experiment, and ⚖️ evaluate different versions of your app (with your code!) in the web platform.
+
+
+
+
diff --git a/agenta-cli/agenta/__init__.py b/agenta-cli/agenta/__init__.py
index 17c91b2aa4..8ab228be22 100644
--- a/agenta-cli/agenta/__init__.py
+++ b/agenta-cli/agenta/__init__.py
@@ -1,12 +1,16 @@
-from . import sdk
-from .sdk import post, ingest, app
+from .sdk.agenta_decorator import app, entrypoint
+from .sdk.context import get_contexts, save_context
from .sdk.types import (
- TextParam,
+ Context,
+ DictInput,
FloatParam,
- IntParam,
InFile,
- Context,
+ IntParam,
MultipleChoiceParam,
- DictInput,
+ MessagesInput,
+ TextParam,
)
-from .sdk.context import save_context, get_contexts
+from .sdk.utils.preinit import PreInitObject
+from .sdk.agenta_init import Config, init
+
+config = PreInitObject("agenta.config", Config)
diff --git a/agenta-cli/agenta/cli/helper.py b/agenta-cli/agenta/cli/helper.py
index d4a72bd14d..17d5cc05e7 100644
--- a/agenta-cli/agenta/cli/helper.py
+++ b/agenta-cli/agenta/cli/helper.py
@@ -1,25 +1,136 @@
-from pathlib import Path
-from typing import Any, List, MutableMapping
-import click
+import sys
import toml
+import click
+import questionary
+from pathlib import Path
from agenta.client import client
+from typing import Any, List, MutableMapping
from agenta.client.api_models import AppVariant
+from typing import Any, Optional
+from pathlib import Path
+import toml
+
+
+def get_global_config(var_name: str) -> Optional[Any]:
+ """
+ Get the value of a global configuration variable.
+
+ Args:
+ var_name: the name of the variable to get
+
+ Returns:
+ the value of the variable, or None if it doesn't exist
+ """
+ agenta_dir = Path.home() / ".agenta"
+ if not agenta_dir.exists():
+ return None
+ agenta_config_file = agenta_dir / "config.toml"
+ if not agenta_config_file.exists():
+ return None
+ global_config = toml.load(agenta_config_file)
+ if var_name not in global_config:
+ return None
+ return global_config[var_name]
+
+
+def set_global_config(var_name: str, var_value: Any) -> None:
+ """
+ Set the value of a global configuration variable.
+
+ Args:
+ var_name: the name of the variable to set
+ var_value: the value to set the variable to
+ """
+ agenta_dir = Path.home() / ".agenta"
+ if not agenta_dir.exists():
+ agenta_dir.mkdir(exist_ok=True)
+ agenta_config_file = agenta_dir / "config.toml"
+ if not agenta_config_file.exists():
+ config = {}
+ with agenta_config_file.open("w") as config_file:
+ toml.dump(config, config_file)
+ global_config = toml.load(agenta_config_file)
+ global_config[var_name] = var_value
+ with open(agenta_config_file, "w") as config_file:
+ toml.dump(global_config, config_file)
+
+
+def get_api_key() -> str:
+ """
+ Retrieve or request the API key for accessing the Agenta platform.
+
+ This function first looks for an existing API key in the global config file.
+ If found, it prompts the user to confirm whether they'd like to use that key.
+ If not found, it asks the user to input a new key.
+
+ Returns:
+ str: The API key to be used for accessing the Agenta platform.
+
+ Raises:
+ SystemExit: If the user cancels the input by pressing Ctrl+C.
+ """
+
+ api_key = get_global_config("api_key")
+ if api_key:
+ # API key exists in the config file, ask for confirmation
+ confirm_api_key = questionary.confirm(
+ f"API Key found: {api_key}\nDo you want to use this API Key?"
+ ).ask()
+
+ if confirm_api_key:
+ return api_key
+ elif confirm_api_key is None: # User pressed Ctrl+C
+ sys.exit(0)
+
+ api_key = questionary.text(
+ "(You can get your API Key here: https://cloud.agenta.ai/settings?tab=apiKeys) "
+ "Please provide your API key:"
+ ).ask()
+
+ if api_key:
+ set_global_config("api_key", api_key)
+
+ return api_key
+ elif api_key is None: # User pressed Ctrl+C
+ sys.exit(0)
+
+
+def init_telemetry_config() -> None:
+ if (
+ get_global_config("telemetry_tracking_enabled") is None
+ or get_global_config("telemetry_api_key") is None
+ ):
+ set_global_config("telemetry_tracking_enabled", True)
+ set_global_config(
+ "telemetry_api_key", "phc_hmVSxIjTW1REBHXgj2aw4HW9X6CXb6FzerBgP9XenC7"
+ )
+
+
def update_variants_from_backend(
- app_name: str, config: MutableMapping[str, Any], host: str
+ app_id: str,
+ config: MutableMapping[str, Any],
+ host: str,
+ api_key: str = None,
) -> MutableMapping[str, Any]:
"""Reads the list of variants from the backend and updates the config accordingly
Arguments:
- app_name -- the app name
+ app_id -- the app id
config -- the config loaded using toml.load
+ api_key -- the api key to use for authentication
Returns:
a new config object later to be saved using toml.dump(config, config_file.open('w'))
"""
- variants: List[AppVariant] = client.list_variants(app_name, host)
+ try:
+ variants: List[AppVariant] = client.list_variants(app_id, host, api_key)
+ except Exception as ex:
+ raise ex
+
config["variants"] = [variant.variant_name for variant in variants]
+ config["variant_ids"] = [variant.variant_id for variant in variants]
return config
@@ -31,10 +142,13 @@ def update_config_from_backend(config_file: Path, host: str):
"""
assert config_file.exists(), "Config file does not exist!"
config = toml.load(config_file)
- app_name = config["app-name"]
+ app_id = config["app_id"]
+ api_key = config.get("api_key", None)
if "variants" not in config:
config["variants"] = []
- config = update_variants_from_backend(app_name, config, host)
+ if "variant_ids" not in config:
+ config["variant_ids"] = []
+ config = update_variants_from_backend(app_id, config, host, api_key)
toml.dump(config, config_file.open("w"))
diff --git a/agenta-cli/agenta/cli/main.py b/agenta-cli/agenta/cli/main.py
index 1e779b83bb..b3dcb1f891 100644
--- a/agenta-cli/agenta/cli/main.py
+++ b/agenta-cli/agenta/cli/main.py
@@ -7,7 +7,10 @@
import click
import questionary
import toml
+
+from agenta.client import client
from agenta.cli import variant_commands
+from agenta.cli import helper
def print_version(ctx, param, value):
@@ -29,7 +32,7 @@ def check_latest_version() -> Union[str, None]:
import requests
try:
- response = requests.get("https://pypi.org/pypi/agenta/json")
+ response = requests.get("https://pypi.org/pypi/agenta/json", timeout=360)
response.raise_for_status()
latest_version = response.json()["info"]["version"]
return latest_version
@@ -85,77 +88,100 @@ def init(app_name: str):
"Invalid input. Please use only alphanumeric characters without spaces."
)
- where_question = questionary.select(
- "Are you running agenta locally?", choices=["Yes", "No"]
- ).ask()
-
- if where_question == "Yes":
- backend_host = "http://localhost"
- elif where_question == "No":
- backend_host = questionary.text(
- "Please provide the IP or URL of your remote host"
+ try:
+ where_question = questionary.select(
+ "Where are you running agenta?",
+ choices=["On agenta cloud", "On my local machine", "On a remote machine"],
).ask()
- elif where_question is None: # User pressed Ctrl+C
- sys.exit(0)
- backend_host = (
- backend_host
- if backend_host.startswith("http://") or backend_host.startswith("https://")
- else "http://" + backend_host
- )
-
- config = {"app-name": app_name, "backend_host": backend_host}
- with open("config.toml", "w") as config_file:
- toml.dump(config, config_file)
-
- # Ask for init option
- init_option = questionary.select(
- "How do you want to initialize your app?",
- choices=["Blank App", "Start from template"],
- ).ask()
-
- # If the user selected the second option, show a list of available templates
- if init_option == "Start from template":
- current_dir = Path.cwd()
- template_dir = Path(__file__).parent.parent / "templates"
- templates = [
- folder.name for folder in template_dir.iterdir() if folder.is_dir()
- ]
- template_desc = [
- toml.load((template_dir / name / "template.toml"))["short_desc"]
- for name in templates
- ]
-
- # Show the templates to the user
- template = questionary.select(
- "Which template do you want to use?",
- choices=[
- questionary.Choice(
- title=f"{template} - {template_desc}", value=template
- )
- for template, template_desc in zip(templates, template_desc)
- ],
+
+ if where_question == "On my local machine":
+ backend_host = "http://localhost"
+ elif where_question == "On a remote machine":
+ backend_host = questionary.text(
+ "Please provide the IP or URL of your remote host"
+ ).ask()
+ elif where_question == "On agenta cloud":
+ backend_host = "https://cloud.agenta.ai"
+ api_key = helper.get_api_key()
+ client.validate_api_key(api_key, backend_host)
+
+ elif where_question is None: # User pressed Ctrl+C
+ sys.exit(0)
+ backend_host = (
+ backend_host
+ if backend_host.startswith("http://") or backend_host.startswith("https://")
+ else "http://" + backend_host
+ )
+
+ # Get app_id after creating new app in the backend server
+ app_id = client.create_new_app(
+ app_name,
+ backend_host,
+ api_key if where_question == "On agenta cloud" else None,
+ )
+
+ # Set app toml configuration
+ config = {
+ "app_name": app_name,
+ "app_id": app_id,
+ "backend_host": backend_host,
+ "api_key": api_key if where_question == "On agenta cloud" else None,
+ }
+ with open("config.toml", "w") as config_file:
+ toml.dump(config, config_file)
+
+ # Ask for init option
+ init_option = questionary.select(
+ "How do you want to initialize your app?",
+ choices=["Blank App", "Start from template"],
).ask()
- # Copy the template files to the current directory
- chosen_template_dir = template_dir / template
- for file in chosen_template_dir.glob("*"):
- if file.name != "template.toml" and not file.is_dir():
- shutil.copy(file, current_dir / file.name)
- elif init_option is None: # User pressed Ctrl+C
- sys.exit(0)
-
- # Create a .gitignore file and add some default environment folder names to it
- gitignore_content = (
- "# Environments \nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\nmyenv/\n"
- )
- with open(".gitignore", "w") as gitignore_file:
- gitignore_file.write(gitignore_content)
-
- click.echo("App initialized successfully")
- if init_option == "Start from template":
- click.echo(
- "Please check the README.md for further instructions to setup the template."
+ # If the user selected the second option, show a list of available templates
+ if init_option == "Start from template":
+ current_dir = Path.cwd()
+ template_dir = Path(__file__).parent.parent / "templates"
+ templates = [
+ folder.name for folder in template_dir.iterdir() if folder.is_dir()
+ ]
+ template_desc = [
+ toml.load((template_dir / name / "template.toml"))["short_desc"]
+ for name in templates
+ ]
+
+ # Show the templates to the user
+ template = questionary.select(
+ "Which template do you want to use?",
+ choices=[
+ questionary.Choice(
+ title=f"{template} - {template_desc}", value=template
+ )
+ for template, template_desc in zip(templates, template_desc)
+ ],
+ ).ask()
+
+ # Copy the template files to the current directory
+ chosen_template_dir = template_dir / template
+ for file in chosen_template_dir.glob("*"):
+ if file.name != "template.toml" and not file.is_dir():
+ shutil.copy(file, current_dir / file.name)
+ elif init_option is None: # User pressed Ctrl+C
+ sys.exit(0)
+
+ # Create a .gitignore file and add some default environment folder names to it
+ gitignore_content = (
+ "# Environments \nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\nmyenv/\n"
)
+ with open(".gitignore", "w") as gitignore_file:
+ gitignore_file.write(gitignore_content)
+
+ click.echo("App initialized successfully")
+ if init_option == "Start from template":
+ click.echo(
+ "Please check the README.md for further instructions to setup the template."
+ )
+ except Exception as ex:
+ click.echo(click.style(f"Error: {ex}", fg="red"))
+ sys.exit(1)
# Add the commands to the CLI group
diff --git a/agenta-cli/agenta/cli/telemetry.py b/agenta-cli/agenta/cli/telemetry.py
new file mode 100644
index 0000000000..a293ec70bd
--- /dev/null
+++ b/agenta-cli/agenta/cli/telemetry.py
@@ -0,0 +1,48 @@
+# Stdlib Imports
+import toml
+from pathlib import Path
+
+# Own Imports
+from agenta.cli import helper
+
+# Third party Imports
+from posthog import Posthog
+
+
+# Load telemetry configuration
+helper.init_telemetry_config()
+
+
+class EventTracking(Posthog):
+ _instance = None
+
+ def __new__(cls, *args, **kwargs):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+
+ def __init__(self, api_key: str, host: str) -> None:
+ super(Posthog, self).__init__(api_key, host)
+
+ def capture_event(
+ self,
+ distinct_id: str,
+ event_name: str,
+ body: dict,
+ ) -> None:
+ """
+ Captures an event.
+
+ Args:
+ distinct_id (str): A unique identifier for the user or entity associated with the event.
+ event_name (str): The name of the event being captured.
+ body (dict): Contains the data associated with the event being captured.
+ """
+
+ self.capture(distinct_id, event_name, body)
+
+
+# Initialize event tracking
+event_track = EventTracking(
+ helper.get_global_config("telemetry_api_key"), "https://app.posthog.com"
+)
diff --git a/agenta-cli/agenta/cli/variant_commands.py b/agenta-cli/agenta/cli/variant_commands.py
index af1cea6946..95104a80a6 100644
--- a/agenta-cli/agenta/cli/variant_commands.py
+++ b/agenta-cli/agenta/cli/variant_commands.py
@@ -1,7 +1,7 @@
import re
import sys
-from pathlib import Path
from typing import List
+from pathlib import Path
from requests.exceptions import ConnectionError
@@ -10,12 +10,9 @@
import toml
from agenta.cli import helper
from agenta.client import client
+from agenta.cli.telemetry import event_track
from agenta.client.api_models import AppVariant, Image
-from agenta.docker.docker_utils import (
- build_and_upload_docker_image,
- build_tar_docker_container,
-)
-from docker.models.images import Image as DockerImage
+from agenta.docker.docker_utils import build_tar_docker_container
@click.group()
@@ -24,23 +21,35 @@ def variant():
pass
-def add_variant(app_folder: str, file_name: str, host: str) -> str:
+def add_variant(
+ app_folder: str, file_name: str, host: str, config_name="default"
+) -> str:
"""
Adds a variant to the backend. Sends the code as a tar to the backend, which then containerizes it and adds it to the backend store.
+ The app variant name to be added is
+ {file_name.removesuffix(".py")}.{config_name}
Args:
variant_name: the name of the variant
app_folder: the folder of the app
- file_name: the name of the file to run
+ file_name: the name of the file to run.
host: the host to use for the variant
+ config_name: the name of the config to use for now it is always default
Returns:
- the name of the variant(useful for serve)
+ the name of the code base and variant(useful for serve)
"""
app_path = Path(app_folder)
config_file = app_path / "config.toml"
config = toml.load(config_file)
- app_name = config["app-name"]
- variant_name = file_name.removesuffix(".py")
+
+ app_name = config["app_name"]
+ app_id = config["app_id"]
+ api_key = config.get("api_key", None)
+
+ config_name = "default"
+ base_name = file_name.removesuffix(".py")
+ variant_name = f"{base_name}.{config_name}"
+
# check files in folder
app_file = app_path / file_name
if not app_file.exists():
@@ -71,17 +80,19 @@ def add_variant(app_folder: str, file_name: str, host: str) -> str:
sys.exit(0)
# Validate variant name
- if not re.match("^[a-zA-Z0-9_]+$", variant_name):
+ if not re.match("^[a-zA-Z0-9_]+$", base_name):
click.echo(
click.style(
- "Invalid input. Please use only alphanumeric characters without spaces.",
+ "Invalid input. Please use only alphanumeric characters without spaces in the filename.",
fg="red",
)
)
sys.exit(0)
# update the config file with the variant names from the backend
+ variant_name = f"{base_name}.{config_name}"
overwrite = False
+
if variant_name in config["variants"]:
overwrite = questionary.confirm(
"This variant already exists. Do you want to overwrite it?"
@@ -90,22 +101,27 @@ def add_variant(app_folder: str, file_name: str, host: str) -> str:
click.echo("Operation cancelled.")
sys.exit(0)
- if not overwrite:
- config["variants"].append(variant_name)
try:
click.echo(
click.style(
- f"Preparing variant {variant_name} into a tar file...", fg="yellow"
+ f"Preparing code base {base_name} into a tar file...",
+ fg="bright_black",
)
)
tar_path = build_tar_docker_container(folder=app_path, file_name=file_name)
click.echo(
click.style(
- f"Building variant {variant_name} into a docker image...", fg="yellow"
+ f"Building code base {base_name} for {variant_name} into a docker image...",
+ fg="bright_black",
)
)
- image: Image = client.send_docker_tar(app_name, variant_name, tar_path, host)
+ image: Image = client.send_docker_tar(
+ app_id, base_name, tar_path, host, api_key
+ )
+ if tar_path.exists():
+ tar_path.unlink()
+
# docker_image: DockerImage = build_and_upload_docker_image(
# folder=app_path, app_name=app_name, variant_name=variant_name)
except Exception as ex:
@@ -115,29 +131,69 @@ def add_variant(app_folder: str, file_name: str, host: str) -> str:
if overwrite:
click.echo(
click.style(
- f"Updating variant {variant_name} to server...", fg="yellow"
+ f"Updating {base_name} to server...",
+ fg="bright_black",
)
)
- client.update_variant_image(app_name, variant_name, image, host)
+ variant_id = config["variant_ids"][config["variants"].index(variant_name)]
+ client.update_variant_image(
+ variant_id, image, host, api_key
+ ) # this automatically restarts
else:
- click.echo(
- click.style(f"Adding variant {variant_name} to server...", fg="yellow")
+ click.echo(click.style(f"Adding {variant_name} to server...", fg="yellow"))
+ response = client.add_variant_to_server(
+ app_id, base_name, image, host, api_key
)
- client.add_variant_to_server(app_name, variant_name, image, host)
+ variant_id = response["variant_id"]
+ config["variants"].append(variant_name)
+ config["variant_ids"].append(variant_id)
except Exception as ex:
if overwrite:
click.echo(click.style(f"Error while updating variant: {ex}", fg="red"))
else:
click.echo(click.style(f"Error while adding variant: {ex}", fg="red"))
return None
+
+ agenta_dir = Path.home() / ".agenta"
+ global_toml_file = toml.load(agenta_dir / "config.toml")
+ tracking_enabled: bool = global_toml_file["telemetry_tracking_enabled"]
if overwrite:
+ # Track a deployment event
+ if tracking_enabled:
+ user_id = client.retrieve_user_id(host, api_key)
+ event_track.capture_event(
+ user_id,
+ "app_deployment",
+ body={
+ "app_id": app_id,
+ "deployed_by": user_id,
+ "environment": "CLI",
+ "version": "cloud" if api_key else "oss",
+ },
+ )
+
click.echo(
click.style(
- f"Variant {variant_name} for App {app_name} updated successfully to Agenta!",
+ f"Variant {variant_name} for App {app_name} updated successfully 🎉",
+ bold=True,
fg="green",
)
)
else:
+ # Track a deployment event
+ if tracking_enabled:
+ user_id = client.retrieve_user_id(host, api_key)
+ event_track.capture_event(
+ user_id,
+ "app_deployment",
+ body={
+ "app_id": app_id,
+ "deployed_by": user_id,
+ "environment": "CLI",
+ "version": "cloud" if api_key else "oss",
+ },
+ )
+
click.echo(
click.style(
f"Variant {variant_name} for App {app_name} added successfully to Agenta!",
@@ -151,10 +207,10 @@ def add_variant(app_folder: str, file_name: str, host: str) -> str:
# TODO: Improve this stupid design
return None
else:
- return variant_name
+ return variant_id
-def start_variant(variant_name: str, app_folder: str, host: str):
+def start_variant(variant_id: str, app_folder: str, host: str):
"""
Starts a container for an existing variant
Args:
@@ -164,17 +220,18 @@ def start_variant(variant_name: str, app_folder: str, host: str):
app_folder = Path(app_folder)
config_file = app_folder / "config.toml"
config = toml.load(config_file)
- app_name = config["app-name"]
+ api_key = config.get("api_key", None)
+ app_id = config["app_id"]
if len(config["variants"]) == 0:
click.echo("No variants found. Please add a variant first.")
return
- if variant_name:
- if variant_name not in config["variants"]:
+ if variant_id:
+ if variant_id not in config["variant_ids"]:
click.echo(
click.style(
- f"Variant {variant_name} not found in backend. Maybe you removed it in the webUI?",
+ f"Variant {variant_id} not found in backend. Maybe you removed it in the webUI?",
fg="red",
)
)
@@ -183,30 +240,30 @@ def start_variant(variant_name: str, app_folder: str, host: str):
variant_name = questionary.select(
"Please choose a variant", choices=config["variants"]
).ask()
+ variant_id = config["variant_ids"][config["variants"].index(variant_name)]
- endpoint = client.start_variant(app_name, variant_name, host=host)
+ endpoint = client.start_variant(variant_id=variant_id, host=host, api_key=api_key)
click.echo("\n" + click.style("Congratulations! 🎉", bold=True, fg="green"))
click.echo(
- click.style(f"Your app has been deployed locally as an API. 🚀", fg="cyan")
- + click.style(f" You can access it here: ", fg="white")
+ click.style("Your app has been deployed locally as an API. 🚀", fg="cyan")
+ + click.style(" You can access it here: ", fg="white")
+ click.style(f"{endpoint}/", bold=True, fg="yellow")
)
click.echo(
- click.style(f"\nRead the API documentation. 📚", fg="cyan")
- + click.style(f" It's available at: ", fg="white")
+ click.style("\nRead the API documentation. 📚", fg="cyan")
+ + click.style(" It's available at: ", fg="white")
+ click.style(f"{endpoint}/docs", bold=True, fg="yellow")
)
- webui_host = "http://localhost:3000" if host == "localhost" else host
+ webui_host = "http://localhost" if host == "localhost" else host
click.echo(
click.style(
- "\nStart experimenting with your app in the playground. 🎮", fg="cyan"
+ "\nStart experimenting with your app in the playground. 🎮",
+ fg="cyan",
)
+ click.style(" Go to: ", fg="white")
- + click.style(
- f"{webui_host}/apps/{app_name}/playground", bold=True, fg="yellow"
- )
+ + click.style(f"{webui_host}/apps/{app_id}/playground", bold=True, fg="yellow")
+ "\n"
)
@@ -220,7 +277,17 @@ def remove_variant(variant_name: str, app_folder: str, host: str):
"""
config_file = Path(app_folder) / "config.toml"
config = toml.load(config_file)
- app_name = config["app-name"]
+ app_name = config["app_name"]
+ api_key = config.get("api_key", None)
+
+ if not config["variants"]:
+ click.echo(
+ click.style(
+ f"No variants found for app {app_name}. Make sure you have deployed at least one variant.",
+ fg="red",
+ )
+ )
+ return
if variant_name:
if variant_name not in config["variants"]:
@@ -235,8 +302,9 @@ def remove_variant(variant_name: str, app_folder: str, host: str):
variant_name = questionary.select(
"Please choose a variant", choices=config["variants"]
).ask()
+ variant_id = config["variant_ids"][config["variants"].index(variant_name)]
try:
- client.remove_variant(app_name, variant_name, host)
+ client.remove_variant(variant_id, host, api_key)
except Exception as ex:
click.echo(
click.style(
@@ -263,8 +331,16 @@ def list_variants(app_folder: str, host: str):
"""
config_file = Path(app_folder) / "config.toml"
config = toml.load(config_file)
- app_name = config["app-name"]
- variants: List[AppVariant] = client.list_variants(app_name, host)
+ app_id = config["app_id"]
+ app_name = config["app_name"]
+ api_key = config.get("api_key", None)
+ variants = []
+
+ try:
+ variants: List[AppVariant] = client.list_variants(app_id, host, api_key)
+ except Exception as ex:
+ raise ex
+
if variants:
for variant in variants:
helper.display_app_variant(variant)
@@ -279,7 +355,7 @@ def config_check(app_folder: str):
app_folder -- the app folder
"""
- click.echo(click.style("\nChecking and updating config file...", fg="yellow"))
+ click.echo(click.style("\nChecking and updating config file...", fg="bright_black"))
app_folder = Path(app_folder)
config_file = app_folder / "config.toml"
if not config_file.exists():
@@ -311,50 +387,84 @@ def get_host(app_folder: str) -> str:
@click.option("--variant_name", default="")
def remove_variant_cli(variant_name: str, app_folder: str):
"""Remove an existing variant."""
- config_check(app_folder)
- remove_variant(
- variant_name=variant_name, app_folder=app_folder, host=get_host(app_folder)
- )
+
+ try:
+ config_check(app_folder)
+ remove_variant(
+ variant_name=variant_name,
+ app_folder=app_folder,
+ host=get_host(app_folder),
+ )
+ except Exception as ex:
+ click.echo(click.style(f"Error while removing variant: {ex}", fg="red"))
-@variant.command(name="serve")
+@variant.command(
+ name="serve",
+ context_settings=dict(
+ ignore_unknown_options=True,
+ allow_extra_args=True,
+ ),
+)
@click.option("--app_folder", default=".")
-@click.option("--file_name", help="The name of the file to run")
-def serve_cli(app_folder: str, file_name: str):
- """Adds a variant to the web ui and serves the api locally."""
+@click.option("--file_name", default=None, help="The name of the file to run")
+@click.pass_context
+def serve_cli(ctx, app_folder: str, file_name: str):
+ """Adds a variant to the web ui and serves the API locally."""
if not file_name:
- error_msg = "To serve variant, kindly provide the filename and run:\n"
- error_msg += ">>> agenta variant serve --file_name .py"
- click.echo(click.style(f"{error_msg}", fg="red"))
- sys.exit(0)
+ if ctx.args:
+ file_name = ctx.args[0]
+ else:
+ error_msg = "To serve variant, kindly provide the filename and run:\n"
+ error_msg += ">>> agenta variant serve --file_name .py\n"
+ error_msg += "or\n"
+ error_msg += ">>> agenta variant serve .py"
+ click.echo(click.style(f"{error_msg}", fg="red"))
+ sys.exit(0)
try:
config_check(app_folder)
+ except Exception as e:
+ click.echo(click.style("Failed during configuration check.", fg="red"))
+ click.echo(click.style(f"Error message: {str(e)}", fg="red"))
+ return
+
+ try:
host = get_host(app_folder)
- variant_name = add_variant(
- app_folder=app_folder, file_name=file_name, host=host
- )
- if (
- variant_name
- ): # otherwise we either failed or we were doing an update and we don't need to manually start the variant!!
- start_variant(variant_name=variant_name, app_folder=app_folder, host=host)
- except ConnectionError:
- error_msg = (
- "Failed to connect to Agenta backend. Here's how you can solve the issue:\n"
- )
- error_msg += "- First, please ensure that the backend service is running and accessible.\n"
- error_msg += (
- "- Second, try restarting the containers (if using Docker Compose)."
- )
- click.echo(click.style(f"{error_msg}", fg="red"))
except Exception as e:
+ click.echo(click.style("Failed to retrieve the host.", fg="red"))
+ click.echo(click.style(f"Error message: {str(e)}", fg="red"))
+ return
+
+ try:
+ variant_id = add_variant(app_folder=app_folder, file_name=file_name, host=host)
+ except Exception as e:
+ click.echo(click.style("Failed to add variant.", fg="red"))
click.echo(click.style(f"Error message: {str(e)}", fg="red"))
+ return
+
+ if variant_id:
+ try:
+ start_variant(variant_id=variant_id, app_folder=app_folder, host=host)
+ except ConnectionError:
+ error_msg = "Failed to connect to Agenta backend. Here's how you can solve the issue:\n"
+ error_msg += "- First, please ensure that the backend service is running and accessible.\n"
+ error_msg += (
+ "- Second, try restarting the containers (if using Docker Compose)."
+ )
+ click.echo(click.style(f"{error_msg}", fg="red"))
+ except Exception as e:
+ click.echo(click.style("Failed to start container with LLM app.", fg="red"))
+ click.echo(click.style(f"Error message: {str(e)}", fg="red"))
@variant.command(name="list")
@click.option("--app_folder", default=".")
def list_variants_cli(app_folder: str):
"""List the variants in the backend"""
- config_check(app_folder)
- list_variants(app_folder=app_folder, host=get_host(app_folder))
+ try:
+ config_check(app_folder)
+ list_variants(app_folder=app_folder, host=get_host(app_folder))
+ except Exception as ex:
+ click.echo(click.style(f"Error while listing variants: {ex}", fg="red"))
diff --git a/agenta-cli/agenta/client/api_models.py b/agenta-cli/agenta/client/api_models.py
index d530909654..6bd5233566 100644
--- a/agenta-cli/agenta/client/api_models.py
+++ b/agenta-cli/agenta/client/api_models.py
@@ -3,16 +3,32 @@
class AppVariant(BaseModel):
+ app_id: str
app_name: str
variant_name: str
+ variant_id: str
parameters: Optional[Dict[str, Any]]
previous_variant_name: Optional[str]
+ base_name: Optional[str]
+ config_name: Optional[str]
+
+
+class Variant(BaseModel):
+ variant_id: str
class Image(BaseModel):
+ type: Optional[str]
docker_id: str
tags: str
class URI(BaseModel):
uri: str
+
+
+class VariantConfigPayload(BaseModel):
+ base_id: str
+ config_name: str
+ parameters: Dict[str, Any]
+ overwrite: bool
diff --git a/agenta-cli/agenta/client/client.py b/agenta-cli/agenta/client/client.py
index 196fce268f..113d798c3f 100644
--- a/agenta-cli/agenta/client/client.py
+++ b/agenta-cli/agenta/client/client.py
@@ -1,147 +1,319 @@
+from typing import Dict, Any, Optional
import os
from pathlib import Path
-from typing import List
+from typing import List, Optional, Dict, Any
-import agenta.config
import requests
-from agenta.client.api_models import AppVariant, Image
+from agenta.client.api_models import AppVariant, Image, VariantConfigPayload
from docker.models.images import Image as DockerImage
+from requests.exceptions import RequestException
-BACKEND_URL_SUFFIX = os.environ["BACKEND_URL_SUFFIX"]
+BACKEND_URL_SUFFIX = os.environ.get("BACKEND_URL_SUFFIX", "api")
class APIRequestError(Exception):
"""Exception to be raised when an API request fails."""
-def add_variant_to_server(app_name: str, variant_name: str, image: Image, host: str):
- """Adds a variant to the server.
+def get_base_by_app_id_and_name(
+ app_id: str, base_name: str, host: str, api_key: str = None
+) -> str:
+ """
+ Get the base ID for a given app ID and base name.
+
+ Args:
+ app_id (str): The ID of the app.
+ base_name (str): The name of the base.
+ host (str): The URL of the server.
+ api_key (str, optional): The API key to use for authentication. Defaults to None.
+
+ Returns:
+ str: The ID of the base.
+
+ Raises:
+ APIRequestError: If the request to get the base fails or the base does not exist on the server.
+ """
+ response = requests.get(
+ f"{host}/{BACKEND_URL_SUFFIX}/bases/?app_id={app_id}&base_name={base_name}",
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+ if response.status_code != 200:
+ error_message = response.json()
+ raise APIRequestError(
+ f"Request to get base failed with status code {response.status_code} and error message: {error_message}."
+ )
+ if len(response.json()) == 0:
+ raise APIRequestError(
+ f"Base with name {base_name} does not exist on the server."
+ )
+ else:
+ return response.json()[0]["base_id"]
- Arguments:
- app_name -- Name of the app
- variant_name -- Name of the variant
- image_name -- Name of the image
+
+def get_app_by_name(app_name: str, host: str, api_key: str = None) -> str:
+ """Get app by its name on the server.
+
+ Args:
+ app_name (str): Name of the app
+ host (str): Hostname of the server
+ api_key (str): The API key to use for the request.
+ """
+
+ response = requests.get(
+ f"{host}/{BACKEND_URL_SUFFIX}/apps/?app_name={app_name}",
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+ if response.status_code != 200:
+ error_message = response.json()
+ raise APIRequestError(
+ f"Request to get app failed with status code {response.status_code} and error message: {error_message}."
+ )
+ if len(response.json()) == 0:
+ raise APIRequestError(f"App with name {app_name} does not exist on the server.")
+ else:
+ return response.json()[0]["app_id"] # only one app should exist for that name
+
+
+def create_new_app(app_name: str, host: str, api_key: str = None) -> str:
+ """Creates new app on the server.
+
+ Args:
+ app_name (str): Name of the app
+ host (str): Hostname of the server
+ api_key (str): The API key to use for the request.
"""
- app_variant: AppVariant = AppVariant(app_name=app_name, variant_name=variant_name)
+
response = requests.post(
- f"{host}/{BACKEND_URL_SUFFIX}/app_variant/add/from_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
+ f"{host}/{BACKEND_URL_SUFFIX}/apps/",
+ json={"app_name": app_name},
+ headers={"Authorization": api_key} if api_key is not None else None,
timeout=600,
)
if response.status_code != 200:
- error_message = response.json()["detail"]
+ error_message = response.json()
raise APIRequestError(
- f"Request to app_variant endpoint failed with status code {response.status_code} and error message: {error_message}."
+ f"Request to create new app failed with status code {response.status_code} and error message: {error_message}."
)
+ return response.json()["app_id"]
-def start_variant(app_name: str, variant_name: str, host: str) -> str:
- """Starts a container with the variant an expose its endpoint
+def add_variant_to_server(
+ app_id: str, base_name: str, image: Image, host: str, api_key: str = None
+) -> Dict:
+ """
+ Adds a variant to the server.
- Arguments:
- app_name --
- variant_name -- _description_
+ Args:
+ app_id (str): The ID of the app to add the variant to.
+ variant_name (str): The name of the variant to add.
+ image (Image): The image to use for the variant.
+ host (str): The host URL of the server.
+ api_key (str): The API key to use for the request.
Returns:
- The endpoint of the container
+ dict: The JSON response from the server.
+ Raises:
+ APIRequestError: If the request to the server fails.
"""
+ variant_name = f"{base_name.lower()}.default"
+ payload = {
+ "variant_name": variant_name,
+ "base_name": base_name.lower(),
+ "config_name": "default",
+ "docker_id": image.docker_id,
+ "tags": image.tags,
+ }
response = requests.post(
- f"{host}/{BACKEND_URL_SUFFIX}/app_variant/start/",
- json={"app_variant": {"app_name": app_name, "variant_name": variant_name}},
+ f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variant/from-image/",
+ json=payload,
+ headers={"Authorization": api_key} if api_key is not None else None,
timeout=600,
)
-
if response.status_code != 200:
- error_message = response.json()["detail"]
+ error_message = response.json()
raise APIRequestError(
- f"Request to start variant endpoint failed with status code {response.status_code} and error message: {error_message}."
+ f"Request to app_variant endpoint failed with status code {response.status_code} and error message: {error_message}."
)
- return response.json()["uri"]
+ return response.json()
-def list_variants(app_name: str, host: str) -> List[AppVariant]:
- """Lists all the variants registered in the backend for an app
+def start_variant(
+ variant_id: str,
+ host: str,
+ env_vars: Optional[Dict[str, str]] = None,
+ api_key: str = None,
+) -> str:
+ """
+ Starts or stops a container with the given variant and exposes its endpoint.
- Arguments:
- app_name -- the app name to which to return all the variants
+ Args:
+ variant_id (str): The ID of the variant.
+ host (str): The host URL.
+ env_vars (Optional[Dict[str, str]]): Optional environment variables to inject into the container.
+ api_key (str): The API key to use for the request.
Returns:
- a list of the variants using the pydantic model
+ str: The endpoint of the container.
+
+ Raises:
+ APIRequestError: If the API request fails.
+ """
+ payload = {}
+ payload["action"] = {"action": "START"}
+ if env_vars:
+ payload["env_vars"] = env_vars
+ try:
+ response = requests.put(
+ f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/",
+ json=payload,
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+ if response.status_code == 404:
+ raise APIRequestError(
+ f"404: Variant with ID {variant_id} does not exist on the server."
+ )
+ elif response.status_code != 200:
+ error_message = response.text
+ raise APIRequestError(
+ f"Request to start variant endpoint failed with status code {response.status_code} and error message: {error_message}."
+ )
+ return response.json().get("uri", "")
+
+ except RequestException as e:
+ raise APIRequestError(f"An error occurred while making the request: {e}")
+
+
+def list_variants(app_id: str, host: str, api_key: str = None) -> List[AppVariant]:
+ """
+ Returns a list of AppVariant objects for a given app_id and host.
+
+ Args:
+ app_id (str): The ID of the app to retrieve variants for.
+ host (str): The URL of the host to make the request to.
+ api_key (str): The API key to use for the request.
+
+ Returns:
+ List[AppVariant]: A list of AppVariant objects for the given app_id and host.
"""
response = requests.get(
- f"{host}/{BACKEND_URL_SUFFIX}/app_variant/list_variants/?app_name={app_name}",
+ f"{host}/{BACKEND_URL_SUFFIX}/apps/{app_id}/variants/",
+ headers={"Authorization": api_key} if api_key is not None else None,
timeout=600,
)
-
# Check for successful request
- if response.status_code != 200:
- error_message = response.json()["detail"]
+ if response.status_code == 403:
+ raise APIRequestError(
+ f"No app by id {app_id} exists or you do not have access to it."
+ )
+ elif response.status_code == 404:
+ raise APIRequestError(
+ f"No app by id {app_id} exists or you do not have access to it."
+ )
+ elif response.status_code != 200:
+ error_message = response.json()
raise APIRequestError(
- f"Request to list_variants endpoint failed with status code {response.status_code} and error message: {error_message}."
+ f"Request to apps endpoint failed with status code {response.status_code} and error message: {error_message}."
)
+
app_variants = response.json()
return [AppVariant(**variant) for variant in app_variants]
-def remove_variant(app_name: str, variant_name: str, host: str):
- """Removes a variant from the backend
+def remove_variant(variant_id: str, host: str, api_key: str = None):
+ """
+ Sends a DELETE request to the Agenta backend to remove a variant with the given ID.
+
+ Args:
+ variant_id (str): The ID of the variant to be removed.
+ host (str): The URL of the Agenta backend.
+ api_key (str): The API key to use for the request.
+
+ Raises:
+ APIRequestError: If the request to the remove_variant endpoint fails.
- Arguments:
- app_name -- the app name
- variant_name -- the variant name
+ Returns:
+ None
"""
- app_variant = AppVariant(app_name=app_name, variant_name=variant_name)
- app_variant_json = app_variant.json()
response = requests.delete(
- f"{host}/{BACKEND_URL_SUFFIX}/app_variant/remove_variant/",
- data=app_variant_json,
- headers={"Content-Type": "application/json"},
+ f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}",
+ headers={
+ "Content-Type": "application/json",
+ "Authorization": api_key if api_key is not None else None,
+ },
timeout=600,
)
# Check for successful request
if response.status_code != 200:
- error_message = response.json()["detail"]
+ error_message = response.json()
raise APIRequestError(
f"Request to remove_variant endpoint failed with status code {response.status_code} and error message: {error_message}"
)
-def update_variant_image(app_name: str, variant_name: str, image: Image, host: str):
- """Adds a variant to the server.
+def update_variant_image(variant_id: str, image: Image, host: str, api_key: str = None):
+ """
+ Update the image of a variant with the given ID.
- Arguments:
- app_name -- Name of the app
- variant_name -- Name of the variant
- image_name -- Name of the image
+ Args:
+ variant_id (str): The ID of the variant to update.
+ image (Image): The new image to set for the variant.
+ host (str): The URL of the host to send the request to.
+ api_key (str): The API key to use for the request.
+
+ Raises:
+ APIRequestError: If the request to update the variant fails.
+
+ Returns:
+ None
"""
- app_variant: AppVariant = AppVariant(app_name=app_name, variant_name=variant_name)
response = requests.put(
- f"{host}/{BACKEND_URL_SUFFIX}/app_variant/update_variant_image/",
- json={"app_variant": app_variant.dict(), "image": image.dict()},
+ f"{host}/{BACKEND_URL_SUFFIX}/variants/{variant_id}/image/",
+ json=image.dict(),
+ headers={"Authorization": api_key} if api_key is not None else None,
timeout=600,
)
if response.status_code != 200:
- error_message = response.json()["detail"]
+ error_message = response.json()
raise APIRequestError(
f"Request to update app_variant failed with status code {response.status_code} and error message: {error_message}."
)
def send_docker_tar(
- app_name: str, variant_name: str, tar_path: Path, host: str
+ app_id: str, base_name: str, tar_path: Path, host: str, api_key: str = None
) -> Image:
+ """
+ Sends a Docker tar file to the specified host to build an image for the given app ID and variant name.
+
+ Args:
+ app_id (str): The ID of the app.
+ base_name (str): The name of the codebase.
+ tar_path (Path): The path to the Docker tar file.
+ host (str): The URL of the host to send the request to.
+ api_key (str): The API key to use for the request.
+
+ Returns:
+ Image: The built Docker image.
+
+ Raises:
+ Exception: If the response status code is 500, indicating that serving the variant failed.
+ """
with tar_path.open("rb") as tar_file:
response = requests.post(
- f"{host}/{BACKEND_URL_SUFFIX}/containers/build_image/?app_name={app_name}&variant_name={variant_name}",
+ f"{host}/{BACKEND_URL_SUFFIX}/containers/build_image/?app_id={app_id}&base_name={base_name}",
files={
"tar_file": tar_file,
},
+ headers={"Authorization": api_key} if api_key is not None else None,
timeout=1200,
)
if response.status_code == 500:
- response_error = response.json()["detail"]
+ response_error = response.json()
error_msg = "Serving the variant failed.\n"
error_msg += f"Log: {response_error}\n"
error_msg += "Here's how you may be able to solve the issue:\n"
@@ -152,3 +324,173 @@ def send_docker_tar(
response.raise_for_status()
image = Image.parse_obj(response.json())
return image
+
+
+def save_variant_config(
+ base_id: str,
+ config_name: str,
+ parameters: Dict[str, Any],
+ overwrite: bool,
+ host: str,
+ api_key: Optional[str] = None,
+) -> None:
+ """
+ Saves a variant configuration to the Agenta backend.
+ If the config already exists, it will be overwritten if the overwrite argument is set to True.
+ If the config does does not exist, a new variant will be created.
+
+ Args:
+ base_id (str): The ID of the base configuration.
+ config_name (str): The name of the variant configuration.
+ parameters (Dict[str, Any]): The parameters of the variant configuration.
+ overwrite (bool): Whether to overwrite an existing variant configuration with the same name.
+ host (str): The URL of the Agenta backend.
+ api_key (Optional[str], optional): The API key to use for authentication. Defaults to None.
+
+ Raises:
+ ValueError: If the 'host' argument is not specified.
+ APIRequestError: If the request to the Agenta backend fails.
+
+ Returns:
+ None
+ """
+ if host is None:
+ raise ValueError("The 'host' is not specified in save_variant_config")
+
+ variant_config = VariantConfigPayload(
+ base_id=base_id,
+ config_name=config_name,
+ parameters=parameters,
+ overwrite=overwrite,
+ )
+ try:
+ response = requests.post(
+ f"{host}/{BACKEND_URL_SUFFIX}/configs/",
+ json=variant_config.dict(),
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+ request = f"POST {host}/{BACKEND_URL_SUFFIX}/configs/ {variant_config.dict()}"
+ # Check for successful request
+ if response.status_code != 200:
+ error_message = response.json().get("detail", "Unknown error")
+ raise APIRequestError(
+ f"Request {request} to save_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}"
+ )
+ except RequestException as e:
+ raise APIRequestError(f"Request failed: {str(e)}")
+
+
+def fetch_variant_config(
+ base_id: str,
+ host: str,
+ config_name: Optional[str] = None,
+ environment_name: Optional[str] = None,
+ api_key: Optional[str] = None,
+) -> Dict[str, Any]:
+ """
+ Fetch a variant configuration from the server.
+
+ Args:
+ base_id (str): ID of the base configuration.
+ config_name (str): Configuration name.
+ environment_name (str): Name of the environment.
+ host (str): The server host URL.
+ api_key (Optional[str], optional): The API key to use for authentication. Defaults to None.
+
+ Raises:
+ APIRequestError: If the API request fails.
+
+ Returns:
+ dict: The requested variant configuration.
+ """
+
+ if host is None:
+ raise ValueError("The 'host' is not specified in fetch_variant_config")
+
+ try:
+ if environment_name:
+ endpoint_params = f"?base_id={base_id}&environment_name={environment_name}"
+ elif config_name:
+ endpoint_params = f"?base_id={base_id}&config_name={config_name}"
+ else:
+ raise ValueError(
+ "Either 'config_name' or 'environment_name' must be specified in fetch_variant_config"
+ )
+ response = requests.get(
+ f"{host}/{BACKEND_URL_SUFFIX}/configs/{endpoint_params}",
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+
+ request = f"GET {host}/{BACKEND_URL_SUFFIX}/configs/ {base_id} {config_name} {environment_name}"
+
+ # Check for successful request
+ if response.status_code != 200:
+ error_message = response.json().get("detail", "Unknown error")
+ raise APIRequestError(
+ f"Request {request} to fetch_variant_config endpoint failed with status code {response.status_code}. Error message: {error_message}"
+ )
+
+ return response.json()
+
+ except RequestException as e:
+ raise APIRequestError(f"Request failed: {str(e)}")
+
+
+def validate_api_key(api_key: str, host: str) -> bool:
+ """
+ Validates an API key with the Agenta backend.
+
+ Args:
+ api_key (str): The API key to validate.
+ host (str): The URL of the Agenta backend.
+
+ Returns:
+ bool: Whether the API key is valid or not.
+ """
+ try:
+ headers = {"Authorization": api_key}
+
+ prefix = api_key.split(".")[0]
+
+ response = requests.get(
+ f"{host}/{BACKEND_URL_SUFFIX}/keys/{prefix}/validate/",
+ headers=headers,
+ timeout=600,
+ )
+ if response.status_code != 200:
+ error_message = response.json()
+ raise APIRequestError(
+ f"Request to validate api key failed with status code {response.status_code} and error message: {error_message}."
+ )
+ return True
+ except RequestException as e:
+ raise APIRequestError(f"An error occurred while making the request: {e}")
+
+
+def retrieve_user_id(host: str, api_key: Optional[str] = None) -> str:
+ """Retrieve user ID from the server.
+
+ Args:
+ host (str): The URL of the Agenta backend
+ api_key (str): The API key to validate with.
+
+ Returns:
+ str: the user ID
+ """
+
+ try:
+ response = requests.get(
+ f"{host}/{BACKEND_URL_SUFFIX}/profile/",
+ headers={"Authorization": api_key} if api_key is not None else None,
+ timeout=600,
+ )
+ if response.status_code != 200:
+ error_message = response.json().get("detail", "Unknown error")
+ raise APIRequestError(
+ f"Request to fetch_user_profile endpoint failed with status code {response.status_code}. Error message: {error_message}"
+ )
+ return response.json()["id"]
+ except RequestException as e:
+ raise APIRequestError(f"Request failed: {str(e)}")
diff --git a/agenta-cli/agenta/config.toml b/agenta-cli/agenta/config.toml
index 7678db62d3..bc5cd73754 100644
--- a/agenta-cli/agenta/config.toml
+++ b/agenta-cli/agenta/config.toml
@@ -2,4 +2,4 @@ docker_registry_url="127.0.0.1:5001"
database_url="localhost:5432"
registry="agenta-server"
backend_url_suffix="api"
-allow_origins="http://localhost:3000,http://localhost:3001,http://demo.agenta.ai"
\ No newline at end of file
+allow_origins="http://localhost:3000,http://localhost:3001,http://cloud.agenta.ai,https://cloud.agenta.ai"
\ No newline at end of file
diff --git a/agenta-cli/agenta/docker/docker-assets/Dockerfile.cloud.template b/agenta-cli/agenta/docker/docker-assets/Dockerfile.cloud.template
new file mode 100644
index 0000000000..12b6032d85
--- /dev/null
+++ b/agenta-cli/agenta/docker/docker-assets/Dockerfile.cloud.template
@@ -0,0 +1,8 @@
+FROM public.ecr.aws/s2t9a1r1/agentaai/lambda_templates_public:main
+
+COPY requirements.txt ${LAMBDA_TASK_ROOT}
+RUN pip install --no-cache-dir --disable-pip-version-check -r requirements.txt
+RUN pip install --no-cache-dir --disable-pip-version-check mangum
+COPY . ${LAMBDA_TASK_ROOT}
+
+CMD [ "lambda_function.handler" ]
diff --git a/agenta-cli/agenta/docker/docker-assets/Dockerfile.template b/agenta-cli/agenta/docker/docker-assets/Dockerfile.template
index 84b34cc0e1..224f306a58 100644
--- a/agenta-cli/agenta/docker/docker-assets/Dockerfile.template
+++ b/agenta-cli/agenta/docker/docker-assets/Dockerfile.template
@@ -1,4 +1,4 @@
-FROM agentaai/templates:main
+FROM agentaai/templates_v2:main
ARG ROOT_PATH=/
ENV ROOT_PATH=${ROOT_PATH}
diff --git a/agenta-cli/agenta/docker/docker-assets/lambda_function.py b/agenta-cli/agenta/docker/docker-assets/lambda_function.py
new file mode 100644
index 0000000000..ca186d6e82
--- /dev/null
+++ b/agenta-cli/agenta/docker/docker-assets/lambda_function.py
@@ -0,0 +1,6 @@
+import agenta
+import _app
+from mangum import Mangum
+
+
+handler = Mangum(agenta.app)
diff --git a/agenta-cli/agenta/docker/docker_utils.py b/agenta-cli/agenta/docker/docker_utils.py
index 94c275073b..ceab4fc638 100644
--- a/agenta-cli/agenta/docker/docker_utils.py
+++ b/agenta-cli/agenta/docker/docker_utils.py
@@ -12,6 +12,8 @@
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
+DEBUG = False
+
def create_dockerfile(out_folder: Path):
"""Creates a dockerfile based on the template in the out_folder.
@@ -25,6 +27,12 @@ def create_dockerfile(out_folder: Path):
)
dockerfile_path = out_folder / "Dockerfile"
shutil.copy(dockerfile_template, dockerfile_path)
+ dockerfile_template = (
+ Path(__file__).parent / "docker-assets" / "Dockerfile.cloud.template"
+ )
+ dockerfile_path = out_folder / "Dockerfile.cloud"
+ shutil.copy(dockerfile_template, dockerfile_path)
+
return dockerfile_path
@@ -41,11 +49,10 @@ def build_tar_docker_container(folder: Path, file_name: Path) -> Path:
if tarfile_path.exists():
tarfile_path.unlink()
- dockerfile_path = create_dockerfile(folder)
- shutil.copytree(
- Path(__file__).parent.parent / "sdk", folder / "agenta", dirs_exist_ok=True
- )
+ create_dockerfile(folder)
+ shutil.copytree(Path(__file__).parent.parent, folder / "agenta", dirs_exist_ok=True)
shutil.copy(Path(__file__).parent / "docker-assets" / "main.py", folder)
+ shutil.copy(Path(__file__).parent / "docker-assets" / "lambda_function.py", folder)
shutil.copy(Path(__file__).parent / "docker-assets" / "entrypoint.sh", folder)
# Read the contents of .gitignore file
@@ -77,6 +84,18 @@ def ignore_patterns(path, names):
# Create the tar.gz file
with tarfile.open(tarfile_path, "w:gz") as tar:
tar.add(temp_path, arcname=folder.name)
+ if not DEBUG:
+ # Clean up - remove specified files and folders
+ for item in ["agenta", "main.py", "lambda_function.py", "entrypoint.sh"]:
+ path = folder / item
+ if path.exists():
+ if path.is_dir():
+ shutil.rmtree(path)
+ else:
+ path.unlink()
+
+ for dockerfile in folder.glob("Dockerfile*"):
+ dockerfile.unlink()
# dockerfile_path.unlink()
return tarfile_path
diff --git a/agenta-cli/agenta/sdk/__init__.py b/agenta-cli/agenta/sdk/__init__.py
index eafacae310..4b4cdc6fe9 100644
--- a/agenta-cli/agenta/sdk/__init__.py
+++ b/agenta-cli/agenta/sdk/__init__.py
@@ -1,14 +1,17 @@
-from . import init # import should always come first
-from . import context
-from . import agenta
-from .agenta import post, ingest, app
+from .utils.preinit import PreInitObject # always the first import!
+from . import agenta_decorator, context, types, utils # noqa: F401
+from .agenta_decorator import app, entrypoint
+from .context import get_contexts, save_context
from .types import (
- TextParam,
- FloatParam,
- InFile,
Context,
- MultipleChoiceParam,
DictInput,
+ FloatParam,
+ InFile,
IntParam,
+ MultipleChoiceParam,
+ TextParam,
+ MessagesInput,
)
-from .context import save_context, get_contexts
+from .agenta_init import Config, init
+
+config = PreInitObject("agenta.config", Config)
diff --git a/agenta-cli/agenta/sdk/agenta.py b/agenta-cli/agenta/sdk/agenta.py
deleted file mode 100644
index 830deea73f..0000000000
--- a/agenta-cli/agenta/sdk/agenta.py
+++ /dev/null
@@ -1,314 +0,0 @@
-"""The code for the Agenta SDK"""
-import argparse
-import functools
-import inspect
-import os
-import sys
-import traceback
-from pathlib import Path
-from tempfile import NamedTemporaryFile
-from typing import Any, Callable, Optional
-
-from fastapi import Depends, FastAPI, UploadFile, Body
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse
-
-from .context import get_contexts, save_context
-from .types import (
- FloatParam,
- InFile,
- TextParam,
- Context,
- MultipleChoiceParam,
- DictInput,
- IntParam,
-)
-from .router import router as router
-
-app = FastAPI()
-
-origins = [
- "http://localhost:3000",
- "http://localhost:3001",
- "http://0.0.0.0:3000",
- "http://0.0.0.0:3001",
-]
-
-app.add_middleware(
- CORSMiddleware,
- allow_origins=origins,
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
-
-app.include_router(router, prefix="")
-
-
-def ingest_file(upfile: UploadFile):
- temp_file = NamedTemporaryFile(delete=False)
- temp_file.write(upfile.file.read())
- temp_file.close()
- return InFile(file_name=upfile.filename, file_path=temp_file.name)
-
-
-def ingest(func: Callable[..., Any]):
- sig = inspect.signature(func)
- func_params = sig.parameters
-
- # find the optional parameters for the app
- app_params = {
- name: param
- for name, param in func_params.items()
- if param.annotation in {TextParam, FloatParam}
- }
- # find the default values for the optional parameters
- for name, param in app_params.items():
- default_value = param.default if param.default is not param.empty else None
- app_params[name] = default_value
-
- ingestible_files = {
- name: param for name, param in func_params.items() if param.annotation is InFile
- }
-
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- for name in ingestible_files:
- if name in kwargs and kwargs[name] is not None:
- kwargs[name] = ingest_file(kwargs[name])
- try:
- return func(*args, **kwargs)
- except Exception as e:
- traceback_str = "".join(
- traceback.format_exception(None, e, e.__traceback__)
- )
- return JSONResponse(
- status_code=500,
- content={"error": str(e), "traceback": traceback_str},
- )
-
- new_params = []
- for name, param in sig.parameters.items():
- if name in app_params:
- new_params.append(
- inspect.Parameter(
- name,
- inspect.Parameter.KEYWORD_ONLY,
- default=Body(app_params[name]),
- annotation=Optional[param.annotation],
- )
- )
- elif name in ingestible_files:
- new_params.append(
- inspect.Parameter(name, param.kind, annotation=UploadFile)
- )
- else:
- new_params.append(
- inspect.Parameter(
- name,
- inspect.Parameter.KEYWORD_ONLY,
- default=Body(...),
- annotation=param.annotation,
- )
- )
-
- wrapper.__signature__ = sig.replace(parameters=new_params)
-
- route = "/ingest"
- app.post(route)(wrapper)
-
- # check if the module is being run as the main script
- if (
- os.path.splitext(os.path.basename(sys.argv[0]))[0]
- == os.path.splitext(os.path.basename(inspect.getfile(func)))[0]
- ):
- parser = argparse.ArgumentParser()
- # add arguments to the command-line parser
- for name, param in sig.parameters.items():
- if name in app_params:
- # For optional parameters, we add them as options
- parser.add_argument(
- f"--{name}",
- type=type(param.default),
- default=param.default,
- )
- elif name in ingestible_files:
- parser.add_argument(name, type=str)
- else:
- # For required parameters, we add them as arguments
- parser.add_argument(name, type=param.annotation)
-
- args = parser.parse_args()
- args_dict = vars(args)
- for name in ingestible_files:
- args_dict[name] = InFile(
- file_name=Path(args_dict[name]).stem, file_path=args_dict[name]
- )
- print(func(**vars(args)))
-
- return wrapper
-
-
-def post(func: Callable[..., Any]):
- endpoint_name = "generate"
- sig = inspect.signature(func)
- func_params = sig.parameters
-
- # determin the optional parameters for the app and save their default values
- app_params = {
- name: param.default if param.default is not param.empty else None
- for name, param in func_params.items()
- if param.annotation
- in {TextParam, FloatParam, IntParam, DictInput, MultipleChoiceParam}
- }
-
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- kwargs = {**app_params, **kwargs}
- try:
- result = func(*args, **kwargs)
- if isinstance(result, Context):
- save_context(result)
- return result
- except Exception as e:
- if sys.version_info.major == 3 and sys.version_info.minor < 10:
- traceback_str = "".join(
- traceback.format_exception(None, e, e.__traceback__)
- )
- else:
- traceback_str = "".join(
- traceback.format_exception(e, value=e, tb=e.__traceback__)
- )
- return JSONResponse(
- status_code=500,
- content={"error": str(e), "traceback": traceback_str},
- )
-
- new_params = []
- for name, param in sig.parameters.items():
- if name in app_params: # optional parameters
- new_params.append(
- inspect.Parameter(
- name,
- inspect.Parameter.KEYWORD_ONLY,
- default=Body(app_params[name]),
- annotation=Optional[param.annotation],
- )
- )
- else: # required parameters
- new_params.append(
- inspect.Parameter(
- name,
- inspect.Parameter.KEYWORD_ONLY,
- default=Body(...),
- annotation=param.annotation,
- )
- )
-
- wrapper.__signature__ = sig.replace(parameters=new_params)
-
- route = f"/{endpoint_name}"
- app.post(route)(wrapper)
- override_schema(
- openapi_schema=app.openapi(),
- func_name=func.__name__,
- endpoint=endpoint_name,
- app_params=app_params,
- )
-
- # check if the module is being run as the main script
- if (
- os.path.splitext(os.path.basename(sys.argv[0]))[0]
- == os.path.splitext(os.path.basename(inspect.getfile(func)))[0]
- ):
- parser = argparse.ArgumentParser()
- # add arguments to the command-line parser
- for name, param in sig.parameters.items():
- if name in app_params:
- if param.annotation is MultipleChoiceParam:
- parser.add_argument(
- f"--{name}",
- type=str,
- default=param.default,
- choices=param.default.choices,
- )
- else:
- parser.add_argument(
- f"--{name}",
- type=type(param.default),
- default=param.default,
- )
- else:
- # For required parameters, we add them as arguments
- parser.add_argument(name, type=param.annotation)
-
- args = parser.parse_args()
- print(func(**vars(args)))
-
- return wrapper
-
-
-def override_schema(
- openapi_schema: dict, func_name: str, endpoint: str, app_params: dict
-):
- """
- Overrides the default openai schema generated by fastapi with additional information about:
- - The choices available for each MultipleChoiceParam instance
- - The min and max values for each FloatParam instance
- - The min and max values for each IntParam instance
- - The default value for DictInput instance
- - ... [PLEASE ADD AT EACH CHANGE]
-
- Args:
- openapi_schema (dict): The openapi schema generated by fastapi
- func_name (str): The name of the function to override
- endpoint (str): The name of the endpoint to override
- app_params (dict(param_name, param_val)): The dictionary of optional parameters for the function
- """
-
- def find_in_schema(schema: dict, param_name: str, xparam: str):
- """Finds a parameter in the schema based on its name and x-parameter value"""
- for _, value in schema.items():
- value_title_lower = str(value.get("title")).lower()
- value_title = (
- "_".join(value_title_lower.split())
- if len(value_title_lower.split()) >= 2
- else value_title_lower
- )
-
- if (
- isinstance(value, dict)
- and value.get("x-parameter") == xparam
- and value_title == param_name
- ):
- return value
-
- schema_to_override = openapi_schema["components"]["schemas"][
- f"Body_{func_name}_{endpoint}_post"
- ]["properties"]
- for param_name, param_val in app_params.items():
- if isinstance(param_val, MultipleChoiceParam):
- subschema = find_in_schema(schema_to_override, param_name, "choice")
- default = str(param_val)
- param_choices = param_val.choices
- choices = (
- [default] + param_choices
- if param_val not in param_choices
- else param_choices
- )
-
- subschema["enum"] = choices
- subschema["default"] = default if default in param_choices else choices[0]
- if isinstance(param_val, FloatParam):
- subschema = find_in_schema(schema_to_override, param_name, "float")
- subschema["minimum"] = param_val.minval
- subschema["maximum"] = param_val.maxval
- subschema["default"] = param_val
- if isinstance(param_val, IntParam):
- subschema = find_in_schema(schema_to_override, param_name, "int")
- subschema["minimum"] = param_val.minval
- subschema["maximum"] = param_val.maxval
- subschema["default"] = param_val
- if isinstance(param_val, DictInput):
- subschema = find_in_schema(schema_to_override, param_name, "dict")
- subschema["default"] = param_val.data
diff --git a/agenta-cli/agenta/sdk/agenta_decorator.py b/agenta-cli/agenta/sdk/agenta_decorator.py
new file mode 100644
index 0000000000..d12b754182
--- /dev/null
+++ b/agenta-cli/agenta/sdk/agenta_decorator.py
@@ -0,0 +1,384 @@
+"""The code for the Agenta SDK"""
+import argparse
+import functools
+import inspect
+import os
+import sys
+import traceback
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+from typing import Any, Callable, Dict, Optional, Tuple
+
+import agenta
+from fastapi import Body, FastAPI, UploadFile
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.openapi.models import Server
+from fastapi.responses import JSONResponse
+
+from .context import get_contexts, save_context
+from .router import router as router
+from .types import (
+ Context,
+ DictInput,
+ FloatParam,
+ InFile,
+ IntParam,
+ MultipleChoiceParam,
+ TextParam,
+ MessagesInput,
+)
+
+app = FastAPI()
+
+origins = [
+ "*",
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(router, prefix="")
+
+
+def ingest_file(upfile: UploadFile):
+ temp_file = NamedTemporaryFile(delete=False)
+ temp_file.write(upfile.file.read())
+ temp_file.close()
+ return InFile(file_name=upfile.filename, file_path=temp_file.name)
+
+
+def entrypoint(func: Callable[..., Any]) -> Callable[..., Any]:
+ """
+ Decorator to wrap a function for HTTP POST and terminal exposure.
+
+ Args:
+ func: Function to wrap.
+
+ Returns:
+ Wrapped function for HTTP POST and terminal.
+ """
+ endpoint_name = "generate"
+ func_signature = inspect.signature(func)
+ config_params = agenta.config.all()
+ ingestible_files = extract_ingestible_files(func_signature)
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs) -> Any:
+ func_params, api_config_params = split_kwargs(kwargs, config_params)
+ ingest_files(func_params, ingestible_files)
+ agenta.config.set(**api_config_params)
+ return execute_function(func, *args, **func_params)
+
+ @functools.wraps(func)
+ def wrapper_deployed(*args, **kwargs) -> Any:
+ func_params = {
+ k: v for k, v in kwargs.items() if k not in ["config", "environment"]
+ }
+ if "environment" in kwargs and kwargs["environment"] is not None:
+ agenta.config.pull(environment_name=kwargs["environment"])
+ elif "config" in kwargs and kwargs["config"] is not None:
+ agenta.config.pull(config_name=kwargs["config"])
+ else: # if no config is specified in the api call, we pull the default config
+ agenta.config.pull(config_name="default")
+ return execute_function(func, *args, **func_params)
+
+ update_function_signature(wrapper, func_signature, config_params, ingestible_files)
+ route = f"/{endpoint_name}"
+ app.post(route)(wrapper)
+
+ update_deployed_function_signature(
+ wrapper_deployed,
+ func_signature,
+ ingestible_files,
+ )
+ route_deployed = f"/{endpoint_name}_deployed"
+ app.post(route_deployed)(wrapper_deployed)
+ override_schema(
+ openapi_schema=app.openapi(),
+ func_name=func.__name__,
+ endpoint=endpoint_name,
+ params={**config_params, **func_signature.parameters},
+ )
+
+ if is_main_script(func):
+ handle_terminal_run(
+ func, func_signature.parameters, config_params, ingestible_files
+ )
+
+ return None
+
+
+def extract_ingestible_files(
+ func_signature: inspect.Signature,
+) -> Dict[str, inspect.Parameter]:
+ """Extract parameters annotated as InFile from function signature."""
+ return {
+ name: param
+ for name, param in func_signature.parameters.items()
+ if param.annotation is InFile
+ }
+
+
+def split_kwargs(
+ kwargs: Dict[str, Any], config_params: Dict[str, Any]
+) -> Tuple[Dict[str, Any], Dict[str, Any]]:
+ """Split keyword arguments into function parameters and API configuration parameters."""
+ func_params = {k: v for k, v in kwargs.items() if k not in config_params}
+ api_config_params = {k: v for k, v in kwargs.items() if k in config_params}
+ return func_params, api_config_params
+
+
+def ingest_files(
+ func_params: Dict[str, Any], ingestible_files: Dict[str, inspect.Parameter]
+) -> None:
+ """Ingest files specified in function parameters."""
+ for name in ingestible_files:
+ if name in func_params and func_params[name] is not None:
+ func_params[name] = ingest_file(func_params[name])
+
+
+def execute_function(func: Callable[..., Any], *args, **func_params) -> Any:
+ """Execute the function and handle any exceptions."""
+ try:
+ result = func(*args, **func_params)
+ if isinstance(result, Context):
+ save_context(result)
+ return result
+ except Exception as e:
+ return handle_exception(e)
+
+
+def handle_exception(e: Exception) -> JSONResponse:
+ """Handle exceptions and return a JSONResponse."""
+ traceback_str = traceback.format_exception(e, value=e, tb=e.__traceback__)
+ return JSONResponse(
+ status_code=500,
+ content={"error": str(e), "traceback": "".join(traceback_str)},
+ )
+
+
+def update_function_signature(
+ wrapper: Callable[..., Any],
+ func_signature: inspect.Signature,
+ config_params: Dict[str, Any],
+ ingestible_files: Dict[str, inspect.Parameter],
+) -> None:
+ """Update the function signature to include new parameters."""
+ updated_params = []
+ add_config_params_to_parser(updated_params, config_params)
+ add_func_params_to_parser(updated_params, func_signature, ingestible_files)
+ wrapper.__signature__ = func_signature.replace(parameters=updated_params)
+
+
+def update_deployed_function_signature(
+ wrapper: Callable[..., Any],
+ func_signature: inspect.Signature,
+ ingestible_files: Dict[str, inspect.Parameter],
+) -> None:
+ """Update the function signature to include new parameters."""
+ updated_params = []
+ add_func_params_to_parser(updated_params, func_signature, ingestible_files)
+ for param in [
+ "config",
+ "environment",
+ ]: # we add the config and environment parameters
+ updated_params.append(
+ inspect.Parameter(
+ param,
+ inspect.Parameter.KEYWORD_ONLY,
+ default=Body(None),
+ annotation=str,
+ )
+ )
+ wrapper.__signature__ = func_signature.replace(parameters=updated_params)
+
+
+def add_config_params_to_parser(
+ updated_params: list, config_params: Dict[str, Any]
+) -> None:
+ """Add configuration parameters to function signature."""
+ for name, param in config_params.items():
+ updated_params.append(
+ inspect.Parameter(
+ name,
+ inspect.Parameter.KEYWORD_ONLY,
+ default=Body(param),
+ annotation=Optional[type(param)],
+ )
+ )
+
+
+def add_func_params_to_parser(
+ updated_params: list,
+ func_signature: inspect.Signature,
+ ingestible_files: Dict[str, inspect.Parameter],
+) -> None:
+ """Add function parameters to function signature."""
+ for name, param in func_signature.parameters.items():
+ if name in ingestible_files:
+ updated_params.append(
+ inspect.Parameter(name, param.kind, annotation=UploadFile)
+ )
+ else:
+ updated_params.append(
+ inspect.Parameter(
+ name,
+ inspect.Parameter.KEYWORD_ONLY,
+ default=Body(..., embed=True),
+ annotation=param.annotation,
+ )
+ )
+
+
+def is_main_script(func: Callable) -> bool:
+ """
+ Check if the script containing the function is the main script being run.
+
+ Args:
+ func (Callable): The function object to check.
+
+ Returns:
+ bool: True if the script containing the function is the main script, False otherwise.
+
+ Example:
+ if is_main_script(my_function):
+ print("This is the main script.")
+ """
+ return (
+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
+ == os.path.splitext(os.path.basename(inspect.getfile(func)))[0]
+ )
+
+
+def handle_terminal_run(
+ func: Callable,
+ func_params: Dict[str, Any],
+ config_params: Dict[str, Any],
+ ingestible_files: Dict,
+) -> None:
+ """
+ Parses command line arguments and sets configuration when script is run from the terminal.
+
+ Args:
+ func_params (dict): A dictionary containing the function parameters and their annotations.
+ config_params (dict): A dictionary containing the configuration parameters.
+
+ Example:
+ handle_terminal_run(func_params=inspect.signature(my_function).parameters, config_params=config.all())
+ """
+ parser = argparse.ArgumentParser()
+ # For required parameters, we add them as arguments
+ for name, param in func_params.items():
+ if name in ingestible_files:
+ parser.add_argument(name, type=str)
+ else:
+ parser.add_argument(name, type=param.annotation)
+ for name, param in config_params.items():
+ if type(param) is MultipleChoiceParam:
+ parser.add_argument(
+ f"--{name}",
+ type=str,
+ default=param.default,
+ choices=param.choices,
+ )
+ else:
+ parser.add_argument(
+ f"--{name}",
+ type=type(param),
+ default=param,
+ )
+
+ args = parser.parse_args()
+
+ # split the arg list into the arg in the app_param and the arge from the sig.parameter
+ args_config_params = {k: v for k, v in vars(args).items() if k in config_params}
+ args_func_params = {k: v for k, v in vars(args).items() if k not in config_params}
+ for name in ingestible_files:
+ args_func_params[name] = InFile(
+ file_name=Path(args_func_params[name]).stem,
+ file_path=args_func_params[name],
+ )
+ agenta.config.set(**args_config_params)
+ # print(func(**args_func_params))
+
+
+def override_schema(openapi_schema: dict, func_name: str, endpoint: str, params: dict):
+ """
+ Overrides the default openai schema generated by fastapi with additional information about:
+ - The choices available for each MultipleChoiceParam instance
+ - The min and max values for each FloatParam instance
+ - The min and max values for each IntParam instance
+ - The default value for DictInput instance
+ - The default value for MessagesParam instance
+ - ... [PLEASE ADD AT EACH CHANGE]
+
+ Args:
+ openapi_schema (dict): The openapi schema generated by fastapi
+ func_name (str): The name of the function to override
+ endpoint (str): The name of the endpoint to override
+ params (dict(param_name, param_val)): The dictionary of the parameters for the function
+ """
+
+ def find_in_schema(schema: dict, param_name: str, xparam: str):
+ """Finds a parameter in the schema based on its name and x-parameter value"""
+ for _, value in schema.items():
+ value_title_lower = str(value.get("title")).lower()
+ value_title = (
+ "_".join(value_title_lower.split())
+ if len(value_title_lower.split()) >= 2
+ else value_title_lower
+ )
+
+ if (
+ isinstance(value, dict)
+ and value.get("x-parameter") == xparam
+ and value_title == param_name
+ ):
+ return value
+
+ schema_to_override = openapi_schema["components"]["schemas"][
+ f"Body_{func_name}_{endpoint}_post"
+ ]["properties"]
+ for param_name, param_val in params.items():
+ # print(param_name, param_val)
+ if isinstance(param_val, MultipleChoiceParam):
+ subschema = find_in_schema(schema_to_override, param_name, "choice")
+ default = str(param_val)
+ param_choices = param_val.choices
+ choices = (
+ [default] + param_choices
+ if param_val not in param_choices
+ else param_choices
+ )
+ subschema["enum"] = choices
+ subschema["default"] = default if default in param_choices else choices[0]
+ if isinstance(param_val, FloatParam):
+ subschema = find_in_schema(schema_to_override, param_name, "float")
+ subschema["minimum"] = param_val.minval
+ subschema["maximum"] = param_val.maxval
+ subschema["default"] = param_val
+ if isinstance(param_val, IntParam):
+ subschema = find_in_schema(schema_to_override, param_name, "int")
+ subschema["minimum"] = param_val.minval
+ subschema["maximum"] = param_val.maxval
+ subschema["default"] = param_val
+ if (
+ isinstance(param_val, inspect.Parameter)
+ and param_val.annotation is DictInput
+ ):
+ subschema = find_in_schema(schema_to_override, param_name, "dict")
+ subschema["default"] = param_val.default["default_keys"]
+ if isinstance(param_val, TextParam):
+ subschema = find_in_schema(schema_to_override, param_name, "text")
+ subschema["default"] = param_val
+ if (
+ isinstance(param_val, inspect.Parameter)
+ and param_val.annotation is MessagesInput
+ ):
+ subschema = find_in_schema(schema_to_override, param_name, "messages")
+ subschema["default"] = param_val.default
diff --git a/agenta-cli/agenta/sdk/agenta_init.py b/agenta-cli/agenta/sdk/agenta_init.py
new file mode 100644
index 0000000000..cc24cf1385
--- /dev/null
+++ b/agenta-cli/agenta/sdk/agenta_init.py
@@ -0,0 +1,189 @@
+import os
+import logging
+from typing import Any, Optional
+from agenta.client import client
+
+from .utils.globals import set_global
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+
+class AgentaSingleton:
+ """Singleton class to save all the "global variables" for the sdk."""
+
+ _instance = None
+ setup = None
+ config = None
+
+ def __new__(cls):
+ if not cls._instance:
+ cls._instance = super(AgentaSingleton, cls).__new__(cls)
+ return cls._instance
+
+ def init(
+ self,
+ app_name: Optional[str] = None,
+ base_name: Optional[str] = None,
+ api_key: Optional[str] = None,
+ base_id: Optional[str] = None,
+ host: Optional[str] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Main function to initialize the singleton.
+
+ Initializes the singleton with the given `app_name`, `base_name`, and `host`. If any of these arguments are not provided,
+ the function will look for them in environment variables.
+
+ Args:
+ app_name (Optional[str]): Name of the Agenta application. Defaults to None. If not provided, will look for "AGENTA_APP_NAME" in environment variables.
+ base_name (Optional[str]): Base name for the Agenta setup. Defaults to None. If not provided, will look for "AGENTA_BASE_NAME" in environment variables.
+ host (Optional[str]): Host name of the backend server. Defaults to None. If not provided, will look for "AGENTA_HOST" in environment variables.
+ kwargs (Any): Additional keyword arguments.
+
+ Raises:
+ ValueError: If `app_name`, `base_name`, or `host` are not specified either as arguments or in the environment variables.
+ """
+ if app_name is None:
+ app_name = os.environ.get("AGENTA_APP_NAME")
+ if base_name is None:
+ base_name = os.environ.get("AGENTA_BASE_NAME")
+ if api_key is None:
+ api_key = os.environ.get("AGENTA_API_KEY")
+ if base_id is None:
+ base_id = os.environ.get("AGENTA_BASE_ID")
+ if host is None:
+ host = os.environ.get("AGENTA_HOST", "http://localhost")
+
+ if base_id is None:
+ if app_name is None or base_name is None:
+ print(
+ f"Warning: Your configuration will not be saved permanently since app_name and base_name are not provided."
+ )
+ else:
+ app_id = client.get_app_by_name(
+ app_name=app_name, host=host, api_key=api_key
+ )
+ base_id = client.get_base_by_app_id_and_name(
+ app_id=app_id, base_name=base_name, host=host, api_key=api_key
+ )
+ self.base_id = base_id
+ self.host = host
+ self.api_key = api_key
+ self.config = Config(base_id=base_id, host=host, api_key=api_key)
+
+
+class Config:
+ def __init__(self, base_id, host, api_key):
+ self.base_id = base_id
+ self.host = host
+ self.api_key = api_key
+ if base_id is None or host is None:
+ self.persist = False
+ else:
+ self.persist = True
+
+ def default(self, overwrite=True, **kwargs):
+ """Saves the default parameters to the app_name and base_name in case they are not already saved.
+ Args:
+ overwrite: Whether to overwrite the existing configuration or not
+ **kwargs: A dict containing the parameters
+ """
+ self.set(
+ **kwargs
+ ) # In case there is no connectivity, we still can use the default values
+ try:
+ self.push(config_name="default", overwrite=overwrite, **kwargs)
+ except Exception as ex:
+ logger.warning(
+ "Unable to push the default configuration to the server." + str(ex)
+ )
+
+ def push(self, config_name: str, overwrite=True, **kwargs):
+ """Pushes the parameters for the app variant to the server
+ Args:
+ config_name: Name of the configuration to push to
+ overwrite: Whether to overwrite the existing configuration or not
+ **kwargs: A dict containing the parameters
+ """
+ if not self.persist:
+ return
+ try:
+ client.save_variant_config(
+ base_id=self.base_id,
+ config_name=config_name,
+ parameters=kwargs,
+ overwrite=overwrite,
+ host=self.host,
+ api_key=self.api_key,
+ )
+ except Exception as ex:
+ logger.warning(
+ "Failed to push the configuration to the server with error: " + str(ex)
+ )
+
+ def pull(self, config_name: str = "default", environment_name: str = None):
+ """Pulls the parameters for the app variant from the server and sets them to the config"""
+ if not self.persist and (
+ config_name != "default" or environment_name is not None
+ ):
+ raise Exception(
+ "Cannot pull the configuration from the server since the app_name and base_name are not provided."
+ )
+ if self.persist:
+ try:
+ if environment_name:
+ config = client.fetch_variant_config(
+ base_id=self.base_id,
+ host=self.host,
+ api_key=self.api_key,
+ environment_name=environment_name,
+ )
+
+ else:
+ config = client.fetch_variant_config(
+ base_id=self.base_id,
+ host=self.host,
+ api_key=self.api_key,
+ config_name=config_name,
+ )
+ except Exception as ex:
+ logger.warning(
+ "Failed to pull the configuration from the server with error: "
+ + str(ex)
+ )
+ try:
+ self.set(**config["parameters"])
+ except Exception as ex:
+ logger.warning("Failed to set the configuration with error: " + str(ex))
+
+ def all(self):
+ """Returns all the parameters for the app variant"""
+ return {
+ k: v
+ for k, v in self.__dict__.items()
+ if k
+ not in ["app_name", "base_name", "host", "base_id", "api_key", "persist"]
+ }
+
+ # function to set the parameters for the app variant
+ def set(self, **kwargs):
+ """Sets the parameters for the app variant
+
+ Args:
+ **kwargs: A dict containing the parameters
+ """
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+
+def init(app_name=None, base_name=None, **kwargs):
+ """Main function to be called by the user to initialize the sdk.
+
+ Args:
+ app_name: _description_. Defaults to None.
+ base_name: _description_. Defaults to None.
+ """
+ singleton = AgentaSingleton()
+ singleton.init(app_name=app_name, base_name=base_name, **kwargs)
+ set_global(setup=singleton.setup, config=singleton.config)
diff --git a/agenta-cli/agenta/sdk/init.py b/agenta-cli/agenta/sdk/init.py
deleted file mode 100644
index 3bd2c4bfbb..0000000000
--- a/agenta-cli/agenta/sdk/init.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from dotenv import load_dotenv
-from .context import setup_db
-
-load_dotenv()
-setup_db()
diff --git a/agenta-cli/agenta/sdk/router.py b/agenta-cli/agenta/sdk/router.py
index 80a7ffa22f..42c766717b 100644
--- a/agenta-cli/agenta/sdk/router.py
+++ b/agenta-cli/agenta/sdk/router.py
@@ -8,3 +8,8 @@
def get_all_contexts():
contexts = get_contexts()
return {"contexts": contexts}
+
+
+@router.get("/health")
+def health():
+ return {"status": "ok"}
diff --git a/agenta-cli/agenta/sdk/types.py b/agenta-cli/agenta/sdk/types.py
index dc57341cdc..5342f3c3ee 100644
--- a/agenta-cli/agenta/sdk/types.py
+++ b/agenta-cli/agenta/sdk/types.py
@@ -84,6 +84,7 @@ def __new__(cls, default: str = None, choices: List[str] = None):
instance = super().__new__(cls, default)
instance.choices = choices
+ instance.default = default
return instance
@classmethod
@@ -97,6 +98,36 @@ def __modify_schema__(cls, field_schema: dict[str, Any]):
)
+class Message(BaseModel):
+ role: str
+ content: str
+
+
+class MessagesInput(list):
+ """Messages Input for Chat-completion.
+
+ Parameters:
+ messages (List[Dict[str, str]]): The list of messages inputs.
+ Required. Each message should be a dictionary with "role" and "content" keys.
+
+ Raises:
+ ValueError: If `messages` is not specified or empty.
+
+ """
+
+ def __new__(cls, messages: List[Dict[str, str]] = None):
+ if not messages:
+ raise ValueError("Missing required parameter in MessagesInput")
+
+ instance = super().__new__(cls, messages)
+ instance.messages = messages
+ return instance
+
+ @classmethod
+ def __modify_schema__(cls, field_schema: dict[str, Any]):
+ field_schema.update({"x-parameter": "messages", "type": "array"})
+
+
class Context(BaseModel):
class Config:
extra = Extra.allow
diff --git a/agenta-cli/agenta/sdk/utils/globals.py b/agenta-cli/agenta/sdk/utils/globals.py
new file mode 100644
index 0000000000..58b8e31bb5
--- /dev/null
+++ b/agenta-cli/agenta/sdk/utils/globals.py
@@ -0,0 +1,14 @@
+import agenta
+
+
+def set_global(setup=None, config=None):
+ """Allows usage of agenta.config and agenta.setup in the user's code.
+
+ Args:
+ setup: _description_. Defaults to None.
+ config: _description_. Defaults to None.
+ """
+ if setup is not None:
+ agenta.setup = setup
+ if config is not None:
+ agenta.config = config
diff --git a/agenta-cli/agenta/sdk/utils/preinit.py b/agenta-cli/agenta/sdk/utils/preinit.py
new file mode 100644
index 0000000000..f039b149f9
--- /dev/null
+++ b/agenta-cli/agenta/sdk/utils/preinit.py
@@ -0,0 +1,41 @@
+from typing import Any, Optional
+from dotenv import load_dotenv
+
+# from .context import setup_db
+
+load_dotenv()
+# setup_db()
+
+
+class PreInitObject:
+ """Dummy object that raises an error when accessed a class before agenta.init() is called."""
+
+ def __init__(self, name: str, destination: Optional[Any] = None) -> None:
+ self._name = name
+
+ if destination is not None:
+ self.__doc__ = destination.__doc__
+
+ def __getitem__(self, key: str) -> None:
+ raise RuntimeError(
+ f"You must call agenta.init() before accessing {self._name}[{key!r}]"
+ )
+
+ def __setitem__(self, key: str, value: Any) -> Any:
+ raise RuntimeError(
+ f"You must call agenta.init() before setting {self._name}[{key!r}]"
+ )
+
+ def __setattr__(self, key: str, value: Any) -> Any:
+ if not key.startswith("_"):
+ raise RuntimeError(
+ f"You must call agenta.init() before {self._name}[{key!r}]"
+ )
+ else:
+ return object.__setattr__(self, key, value)
+
+ def __getattr__(self, key: str) -> Any:
+ if not key.startswith("_"):
+ raise RuntimeError(f"You must call agenta.init() before {self._name}.{key}")
+ else:
+ raise AttributeError
diff --git a/agenta-cli/agenta/templates/compose_email/README.md b/agenta-cli/agenta/templates/compose_email/README.md
new file mode 100644
index 0000000000..757455e2ca
--- /dev/null
+++ b/agenta-cli/agenta/templates/compose_email/README.md
@@ -0,0 +1,9 @@
+# Using this template
+
+Please make sure to create a `.env` file with your OpenAI API key before running the app.
+OPENAI_API_KEY=sk-xxxxxxx
+
+You can find your keys here:
+https://platform.openai.com/account/api-keys
+
+Go back to the [Getting started tutorial](https://docs.agenta.ai/getting-started) to continue
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/compose_email/app.py b/agenta-cli/agenta/templates/compose_email/app.py
new file mode 100644
index 0000000000..3883a31725
--- /dev/null
+++ b/agenta-cli/agenta/templates/compose_email/app.py
@@ -0,0 +1,63 @@
+import agenta as ag
+from langchain.chains import LLMChain
+from langchain.llms import OpenAI
+from langchain.prompts import PromptTemplate
+
+default_prompt = """
+**Write an email** from {from_sender} to {to_receiver} with the designated tone and style: {email_style}. The primary content of the email is: {email_content}.
+
+Use the following format:
+Subject:
+
+
+
+**Procedure**:
+
+**(1) Determine the primary talking points of the email:**
+1. Identify the central theme of the provided content.
+2. Extract secondary messages or supporting points.
+
+**(2) Frame sentences for each talking point, keeping in mind the given tone and style {{ style }}:**
+3. Create a compelling opening sentence that sets the tone and introduces the main theme.
+4. Formulate sentences that add depth or context to each of the previously identified talking points.
+
+**(3) Draft the initial version of the email:**
+Use the sentences crafted in the previous step to compose a coherent and engaging email. Ensure that the flow feels natural and that each sentence transitions smoothly to the next.
+
+**(4) Analyze the email and list ways to refine it:**
+5. Identify areas where the message might be unclear or could benefit from additional information.
+6. Consider places where the language or tone might be enhanced to be more persuasive or emotive.
+7. Evaluate if the email adheres to the style directive and, if not, identify deviations.
+
+**(5) Re-write the email by applying the insights gained:**
+Rework the initial draft, incorporating the improvements identified in the previous step. Aim to present the message as effectively as possible while strictly adhering to the prescribed tone and style.
+
+"""
+
+ag.init()
+ag.config.default(
+ temperature=ag.FloatParam(0.9), prompt_template=ag.TextParam(default_prompt)
+)
+
+
+@ag.entrypoint
+def generate(
+ from_sender: str,
+ to_receiver: str,
+ email_style: str,
+ email_content: str,
+) -> str:
+ llm = OpenAI(temperature=ag.config.temperature)
+ prompt = PromptTemplate(
+ input_variables=["from_sender", "to_receiver", "email_style", "email_content"],
+ template=ag.config.prompt_template,
+ )
+ chain = LLMChain(llm=llm, prompt=prompt)
+ output = chain.run(
+ from_sender=from_sender,
+ to_receiver=to_receiver,
+ email_style=email_style,
+ email_content=email_content,
+ )
+
+ return output
diff --git a/agenta-cli/agenta/templates/compose_email/env.example b/agenta-cli/agenta/templates/compose_email/env.example
new file mode 100644
index 0000000000..0bd3c56d64
--- /dev/null
+++ b/agenta-cli/agenta/templates/compose_email/env.example
@@ -0,0 +1,2 @@
+# Rename this file to .env
+OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/agenta-cli/agenta/templates/compose_email/requirements.txt b/agenta-cli/agenta/templates/compose_email/requirements.txt
new file mode 100644
index 0000000000..2f1e01b99f
--- /dev/null
+++ b/agenta-cli/agenta/templates/compose_email/requirements.txt
@@ -0,0 +1,3 @@
+langchain
+openai
+agenta
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/compose_email/template.toml b/agenta-cli/agenta/templates/compose_email/template.toml
new file mode 100644
index 0000000000..3f5f2b503a
--- /dev/null
+++ b/agenta-cli/agenta/templates/compose_email/template.toml
@@ -0,0 +1 @@
+short_desc="Simple app that composes an email."
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/extract_data_to_json/README.md b/agenta-cli/agenta/templates/extract_data_to_json/README.md
new file mode 100644
index 0000000000..757455e2ca
--- /dev/null
+++ b/agenta-cli/agenta/templates/extract_data_to_json/README.md
@@ -0,0 +1,9 @@
+# Using this template
+
+Please make sure to create a `.env` file with your OpenAI API key before running the app.
+OPENAI_API_KEY=sk-xxxxxxx
+
+You can find your keys here:
+https://platform.openai.com/account/api-keys
+
+Go back to the [Getting started tutorial](https://docs.agenta.ai/getting-started) to continue
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/extract_data_to_json/app.py b/agenta-cli/agenta/templates/extract_data_to_json/app.py
new file mode 100644
index 0000000000..633d59d38c
--- /dev/null
+++ b/agenta-cli/agenta/templates/extract_data_to_json/app.py
@@ -0,0 +1,53 @@
+import agenta as ag
+from openai import OpenAI
+
+client = OpenAI()
+import json
+
+default_prompt = """You are a world class algorithm for extracting information in structured formats. Extract information and create a valid JSON from the following input: {text}"""
+function_json_string = """
+{
+ "name": "extract_information",
+ "description": "Extract information from user-provided text",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "The text to extract information from"
+ }
+ }
+ }
+}
+"""
+
+ag.init()
+ag.config.default(
+ temperature=ag.FloatParam(0.9),
+ prompt_template=ag.TextParam(default_prompt),
+ function_json=ag.TextParam(function_json_string),
+)
+
+
+@ag.entrypoint
+def generate(
+ text: str,
+) -> str:
+ messages = [
+ {
+ "role": "user",
+ "content": ag.config.prompt_template.format(text=text),
+ },
+ ]
+
+ function = json.loads(ag.config.function_json)
+
+ response = client.chat.completions.create(
+ model="gpt-3.5-turbo-0613",
+ messages=messages,
+ temperature=ag.config.temperature,
+ functions=[function],
+ )
+
+ output = str(response["choices"][0]["message"]["function_call"])
+ return output
diff --git a/agenta-cli/agenta/templates/extract_data_to_json/env.example b/agenta-cli/agenta/templates/extract_data_to_json/env.example
new file mode 100644
index 0000000000..0bd3c56d64
--- /dev/null
+++ b/agenta-cli/agenta/templates/extract_data_to_json/env.example
@@ -0,0 +1,2 @@
+# Rename this file to .env
+OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/agenta-cli/agenta/templates/extract_data_to_json/requirements.txt b/agenta-cli/agenta/templates/extract_data_to_json/requirements.txt
new file mode 100644
index 0000000000..2f1e01b99f
--- /dev/null
+++ b/agenta-cli/agenta/templates/extract_data_to_json/requirements.txt
@@ -0,0 +1,3 @@
+langchain
+openai
+agenta
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/extract_data_to_json/template.toml b/agenta-cli/agenta/templates/extract_data_to_json/template.toml
new file mode 100644
index 0000000000..41a613d378
--- /dev/null
+++ b/agenta-cli/agenta/templates/extract_data_to_json/template.toml
@@ -0,0 +1 @@
+short_desc="Simple app that extracts data to JSON from text"
\ No newline at end of file
diff --git a/agenta-cli/agenta/templates/simple_prompt/app.py b/agenta-cli/agenta/templates/simple_prompt/app.py
index e164d31c3e..8cb809ee55 100644
--- a/agenta-cli/agenta/templates/simple_prompt/app.py
+++ b/agenta-cli/agenta/templates/simple_prompt/app.py
@@ -5,17 +5,18 @@
default_prompt = "What is a good name for a company that makes {product}?"
+ag.init()
+ag.config.default(
+ temperature=ag.FloatParam(0.9),
+ prompt_template=ag.TextParam(default_prompt),
+)
-@ag.post
-def generate(
- product: str,
- temperature: ag.FloatParam = 0.9,
- prompt_template: ag.TextParam = default_prompt,
-) -> str:
- llm = OpenAI(temperature=temperature)
+
+@ag.entrypoint
+def generate(product: str) -> str:
+ llm = OpenAI(temperature=ag.config.temperature)
prompt = PromptTemplate(
- input_variables=["product"],
- template=prompt_template,
+ input_variables=["product"], template=ag.config.prompt_template
)
chain = LLMChain(llm=llm, prompt=prompt)
output = chain.run(product=product)
diff --git a/agenta-cli/poetry.lock b/agenta-cli/poetry.lock
index e813aaf546..940e1ffff7 100644
--- a/agenta-cli/poetry.lock
+++ b/agenta-cli/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "anyio"
version = "3.6.2"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
-category = "main"
optional = false
python-versions = ">=3.6.2"
files = [
@@ -25,7 +24,6 @@ trio = ["trio (>=0.16,<0.22)"]
name = "appnope"
version = "0.1.3"
description = "Disable App Nap on macOS >= 10.9"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -37,7 +35,6 @@ files = [
name = "asttokens"
version = "2.2.1"
description = "Annotate AST trees with source code positions"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -55,7 +52,6 @@ test = ["astroid", "pytest"]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
-category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -66,7 +62,6 @@ files = [
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -85,7 +80,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "backcall"
version = "0.2.0"
description = "Specifications for callback functions passed in to an API"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -93,11 +87,21 @@ files = [
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
+[[package]]
+name = "backoff"
+version = "2.2.1"
+description = "Function decoration for backoff and retry"
+optional = false
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
+ {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
+]
+
[[package]]
name = "certifi"
version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -109,7 +113,6 @@ files = [
name = "charset-normalizer"
version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@@ -194,7 +197,6 @@ files = [
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -209,7 +211,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
-category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -221,7 +222,6 @@ files = [
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
-category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -233,7 +233,6 @@ files = [
name = "docker"
version = "6.1.1"
description = "A Python library for the Docker Engine API."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -255,7 +254,6 @@ ssh = ["paramiko (>=2.4.3)"]
name = "executing"
version = "1.2.0"
description = "Get the currently executing AST node of a frame, and other information"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -270,7 +268,6 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
name = "fastapi"
version = "0.95.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -292,7 +289,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -304,7 +300,6 @@ files = [
name = "importlib-metadata"
version = "6.7.0"
description = "Read metadata from Python packages"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -324,7 +319,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
-category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -336,7 +330,6 @@ files = [
name = "ipdb"
version = "0.13.13"
description = "IPython-enabled pdb"
-category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -353,7 +346,6 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version <
name = "ipython"
version = "8.13.2"
description = "IPython: Productive Interactive Computing"
-category = "main"
optional = false
python-versions = ">=3.9"
files = [
@@ -393,7 +385,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa
name = "jedi"
version = "0.18.2"
description = "An autocompletion tool for Python that can be used for text editors."
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -413,7 +404,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
name = "matplotlib-inline"
version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
-category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -424,11 +414,21 @@ files = [
[package.dependencies]
traitlets = "*"
+[[package]]
+name = "monotonic"
+version = "1.6"
+description = "An implementation of time.monotonic() for Python 2 & < 3.3"
+optional = false
+python-versions = "*"
+files = [
+ {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"},
+ {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"},
+]
+
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -440,7 +440,6 @@ files = [
name = "parso"
version = "0.8.3"
description = "A Python Parser"
-category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -456,7 +455,6 @@ testing = ["docopt", "pytest (<6.0.0)"]
name = "pexpect"
version = "4.8.0"
description = "Pexpect allows easy control of interactive console applications."
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -471,7 +469,6 @@ ptyprocess = ">=0.5"
name = "pickleshare"
version = "0.7.5"
description = "Tiny 'shelve'-like database with concurrency support"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -483,7 +480,6 @@ files = [
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -495,11 +491,33 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
+[[package]]
+name = "posthog"
+version = "3.0.2"
+description = "Integrate PostHog into any python application."
+optional = false
+python-versions = "*"
+files = [
+ {file = "posthog-3.0.2-py2.py3-none-any.whl", hash = "sha256:a8c0af6f2401fbe50f90e68c4143d0824b54e872de036b1c2f23b5abb39d88ce"},
+ {file = "posthog-3.0.2.tar.gz", hash = "sha256:701fba6e446a4de687c6e861b587e7b7741955ad624bf34fe013c06a0fec6fb3"},
+]
+
+[package.dependencies]
+backoff = ">=1.10.0"
+monotonic = ">=1.5"
+python-dateutil = ">2.1"
+requests = ">=2.7,<3.0"
+six = ">=1.5"
+
+[package.extras]
+dev = ["black", "flake8", "flake8-print", "isort", "pre-commit"]
+sentry = ["django", "sentry-sdk"]
+test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest"]
+
[[package]]
name = "prompt-toolkit"
version = "3.0.38"
description = "Library for building powerful interactive command lines in Python"
-category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@@ -514,7 +532,6 @@ wcwidth = "*"
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -526,7 +543,6 @@ files = [
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -541,7 +557,6 @@ tests = ["pytest"]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
@@ -553,7 +568,6 @@ files = [
name = "pydantic"
version = "1.10.7"
description = "Data validation and settings management using python type hints"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -606,7 +620,6 @@ email = ["email-validator (>=1.0.3)"]
name = "pygments"
version = "2.15.1"
description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -621,7 +634,6 @@ plugins = ["importlib-metadata"]
name = "pytest"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
-category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -642,11 +654,24 @@ toml = "*"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
[[package]]
name = "python-dotenv"
version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
-category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -661,7 +686,6 @@ cli = ["click (>=5.0)"]
name = "python-multipart"
version = "0.0.6"
description = "A streaming multipart parser for Python"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -676,7 +700,6 @@ dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatc
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -700,7 +723,6 @@ files = [
name = "questionary"
version = "1.10.0"
description = "Python library to build pretty command line user prompts ⭐️"
-category = "main"
optional = false
python-versions = ">=3.6,<4.0"
files = [
@@ -718,7 +740,6 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin
name = "requests"
version = "2.30.0"
description = "Python HTTP for Humans."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -740,7 +761,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
-category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -752,7 +772,6 @@ files = [
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -764,7 +783,6 @@ files = [
name = "stack-data"
version = "0.6.2"
description = "Extract data from python stack frames and tracebacks for informative displays"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -784,7 +802,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
name = "starlette"
version = "0.26.1"
description = "The little ASGI library that shines."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -803,7 +820,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
-category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -815,7 +831,6 @@ files = [
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -827,7 +842,6 @@ files = [
name = "traitlets"
version = "5.9.0"
description = "Traitlets Python configuration system"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -843,7 +857,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
name = "typing-extensions"
version = "4.5.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -855,7 +868,6 @@ files = [
name = "urllib3"
version = "2.0.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -873,7 +885,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "wcwidth"
version = "0.2.6"
description = "Measures the displayed width of unicode strings in a terminal"
-category = "main"
optional = false
python-versions = "*"
files = [
@@ -885,7 +896,6 @@ files = [
name = "websocket-client"
version = "1.5.1"
description = "WebSocket client for Python with low level API options"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -902,7 +912,6 @@ test = ["websockets"]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -917,4 +926,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "91562bdb73e6f78891b1414a5e95c551fa93f6f7cf4dcb22105089b6034e253c"
+content-hash = "1a41135d3e71717b16a1d1d3d1b8523274ae2f816ca9ffceea585a69bc6420dd"
diff --git a/agenta-cli/pyproject.toml b/agenta-cli/pyproject.toml
index f5ce593113..c5f2704efd 100644
--- a/agenta-cli/pyproject.toml
+++ b/agenta-cli/pyproject.toml
@@ -1,9 +1,19 @@
[tool.poetry]
name = "agenta"
-version = "0.2.0"
-description = "Code for the SDK and CLI for the Agenta Lab platform"
-authors = ["Mahmoud Mabrouk "]
+version = "0.5.7"
+description = "The SDK for agenta is an open-source LLMOps platform."
readme = "README.md"
+authors = ["Mahmoud Mabrouk "]
+classifiers = [
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3.9",
+ "Topic :: Software Development :: Libraries"
+]
+homepage = "https://agenta.ai"
+repository = "https://github.com/agenta-ai/agenta"
+documentation = "https://docs.agenta.ai"
+keywords = ["LLMOps", "LLM", "evaluation", "prompt engineering"]
[tool.poetry.dependencies]
python = "^3.9"
@@ -16,6 +26,7 @@ ipdb = "^0.13.13"
python-dotenv = "^1.0.0"
python-multipart = "^0.0.6"
importlib-metadata = "^6.7.0"
+posthog = "^3.0.2"
[tool.poetry.dev-dependencies]
pytest = "^6.2"
diff --git a/agenta-cli/tests/example_projects/test2/agenta.py b/agenta-cli/tests/example_projects/test2/agenta.py
index 87b02f5231..f81e783ec9 100644
--- a/agenta-cli/tests/example_projects/test2/agenta.py
+++ b/agenta-cli/tests/example_projects/test2/agenta.py
@@ -29,14 +29,12 @@
class TextParam(str):
-
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update({"x-parameter": "text"})
class FloatParam(float):
-
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update({"x-parameter": "float"})
@@ -48,8 +46,11 @@ def post(func: Callable[..., Any]):
func_params = sig.parameters
# find the optional parameters for the app
- app_params = {name: param for name, param in func_params.items()
- if param.annotation in {TextParam, FloatParam}}
+ app_params = {
+ name: param
+ for name, param in func_params.items()
+ if param.annotation in {TextParam, FloatParam}
+ }
# find the default values for the optional parameters
for name, param in app_params.items():
default_value = param.default if param.default is not param.empty else None
@@ -61,8 +62,12 @@ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
- traceback_str = ''.join(traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__))
- return JSONResponse(status_code=500, content={"error": str(e), "traceback": traceback_str})
+ traceback_str = "".join(
+ traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__)
+ )
+ return JSONResponse(
+ status_code=500, content={"error": str(e), "traceback": traceback_str}
+ )
new_params = []
for name, param in sig.parameters.items():
@@ -72,8 +77,7 @@ def wrapper(*args, **kwargs):
name,
inspect.Parameter.KEYWORD_ONLY,
default=app_params[name],
- annotation=Optional[param.annotation]
-
+ annotation=Optional[param.annotation],
)
)
else:
@@ -85,14 +89,18 @@ def wrapper(*args, **kwargs):
app.post(route)(wrapper)
# check if the module is being run as the main script
- if os.path.splitext(os.path.basename(sys.argv[0]))[0] == os.path.splitext(os.path.basename(inspect.getfile(func)))[0]:
+ if (
+ os.path.splitext(os.path.basename(sys.argv[0]))[0]
+ == os.path.splitext(os.path.basename(inspect.getfile(func)))[0]
+ ):
parser = argparse.ArgumentParser()
# add arguments to the command-line parser
for name, param in sig.parameters.items():
if name in app_params:
# For optional parameters, we add them as options
- parser.add_argument(f"--{name}", type=type(param.default),
- default=param.default)
+ parser.add_argument(
+ f"--{name}", type=type(param.default), default=param.default
+ )
else:
# For required parameters, we add them as arguments
parser.add_argument(name, type=param.annotation)
diff --git a/agenta-web/.env b/agenta-web/.env
new file mode 100644
index 0000000000..7f796a0946
--- /dev/null
+++ b/agenta-web/.env
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_AGENTA_API_URL=http://localhost
+NEXT_PUBLIC_FF=oss
+NEXT_PUBLIC_TELEMETRY_TRACKING_ENABLED=true
\ No newline at end of file
diff --git a/agenta-web/.env.development.sample b/agenta-web/.env.development.sample
deleted file mode 100644
index 1e0e6f3c86..0000000000
--- a/agenta-web/.env.development.sample
+++ /dev/null
@@ -1 +0,0 @@
-NEXT_PUBLIC_AGENTA_API_URL=
\ No newline at end of file
diff --git a/agenta-web/.env.local.example b/agenta-web/.env.local.example
new file mode 100644
index 0000000000..4716e2b2bf
--- /dev/null
+++ b/agenta-web/.env.local.example
@@ -0,0 +1,4 @@
+# Rename this file to .env.local and provide the secret values to access them in the app
+# .env.local should not be committed to git
+
+NEXT_PUBLIC_OPENAI_API_KEY=
\ No newline at end of file
diff --git a/agenta-web/.env.production.sample b/agenta-web/.env.production.sample
deleted file mode 100644
index 35b37fed51..0000000000
--- a/agenta-web/.env.production.sample
+++ /dev/null
@@ -1 +0,0 @@
-NEXT_PUBLIC_API_URL=https://api-production.com
\ No newline at end of file
diff --git a/agenta-web/.eslintrc.json b/agenta-web/.eslintrc.json
index 520cc3bb57..188940439e 100644
--- a/agenta-web/.eslintrc.json
+++ b/agenta-web/.eslintrc.json
@@ -1,3 +1,7 @@
{
- "extends": ["next/core-web-vitals", "prettier"]
+ "extends": ["next/core-web-vitals"],
+ "rules": {
+ "react/no-unescaped-entities": 0,
+ "react/display-name": 0
+ }
}
diff --git a/agenta-web/.gitignore b/agenta-web/.gitignore
index 2902ce128c..a433da9509 100644
--- a/agenta-web/.gitignore
+++ b/agenta-web/.gitignore
@@ -36,3 +36,7 @@ next-env.d.ts
# cypress videos
cypress/videos
+cypress/screenshots
+
+#Allow
+!.env
diff --git a/agenta-web/cypress.config.ts b/agenta-web/cypress.config.ts
index 3df1f5b2e7..6824bd5527 100644
--- a/agenta-web/cypress.config.ts
+++ b/agenta-web/cypress.config.ts
@@ -1,11 +1,20 @@
import {defineConfig} from "cypress"
+import {config} from "dotenv"
+
+// read in the environment variables from .env.local file
+config({path: ".env.local"})
export default defineConfig({
+ video: false,
+ screenshotOnRunFailure: false,
e2e: {
- baseUrl: "http://localhost:3000",
- defaultCommandTimeout: 6000,
+ baseUrl: "http://localhost",
+ defaultCommandTimeout: 30000,
+ requestTimeout: 10000,
},
env: {
baseApiURL: "http://localhost/api",
+ NEXT_PUBLIC_OPENAI_API_KEY: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
+ NEXT_PUBLIC_FF: false,
},
})
diff --git a/agenta-web/cypress/e2e/ab-testing-evaluation.cy.ts b/agenta-web/cypress/e2e/ab-testing-evaluation.cy.ts
new file mode 100644
index 0000000000..a54245d469
--- /dev/null
+++ b/agenta-web/cypress/e2e/ab-testing-evaluation.cy.ts
@@ -0,0 +1,87 @@
+import {randString} from "../../src/lib/helpers/utils"
+
+describe("A/B Testing Evaluation workflow", () => {
+ let app_v2 = randString(5)
+ let app_id
+ before(() => {
+ cy.createVariantsAndTestsets()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ })
+
+ context("When creating an app variant", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/playground`)
+ })
+
+ it("Should successfully create a new app variant", () => {
+ cy.clickLinkAndWait("button.ant-tabs-nav-add")
+ cy.get('[data-cy="new-variant-modal"]').should("exist")
+ cy.get('[data-cy="new-variant-modal-select"]').click()
+ cy.get('[data-cy^="new-variant-modal-label"]').contains("app.default").click()
+ cy.get('[data-cy="new-variant-modal-input"]').type(app_v2)
+ cy.get('[data-cy="new-variant-modal"]').within(() => {
+ cy.get("button.ant-btn").contains(/ok/i).click()
+ })
+ cy.url().should("include", `/playground?variant=app.${app_v2}`)
+ cy.get('[data-cy="playground-save-changes-button"]').eq(1).click()
+ cy.get('[data-cy="playground-publish-button"]').should("exist")
+ cy.get(".ant-message-notice-content").should("exist")
+ })
+
+ it("Should verify user has more than one app variant", () => {
+ cy.get(".ant-tabs-nav-list").within(() => {
+ cy.get(".ant-tabs-tab").should("have.length.gt", 1)
+ })
+ })
+ })
+
+ context("When executing the evaluation", () => {
+ it("Should successfully execute the evaluation process", () => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.url().should("include", "/evaluations")
+ cy.clickLinkAndWait('[data-cy="abTesting-button"]')
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get(".ant-dropdown")
+ .eq(0)
+ .within(() => {
+ cy.get('[data-cy="variant-0"]').contains("app.default").click()
+ })
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="variants-dropdown-1"]').trigger("mouseover")
+ cy.get(".ant-dropdown")
+ .eq(1)
+ .within(() => {
+ cy.get('[data-cy="variant-1"]').contains(`app.${app_v2}`).click()
+ })
+ cy.get('[data-cy="variants-dropdown-1"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get(".ant-dropdown")
+ .eq(2)
+ .within(() => {
+ cy.get('[data-cy="testset-0"]').click()
+ })
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ cy.url().should("include", "/human_a_b_testing")
+ cy.wait(1000)
+ cy.get('[data-cy="abTesting-run-all-button"]').click()
+ cy.get('[data-cy^="abTesting-app-variant-1-vote-button"]').should("not.be.disabled")
+ cy.get('[data-cy^="abTesting-app-variant-2-vote-button"]').should("not.be.disabled")
+ cy.get('[data-cy^="abTesting-both-bad-vote-button"]').should("not.be.disabled")
+
+ cy.get('[data-cy="abTesting-app-variant-1-vote-button-0"]').click()
+ cy.get('[data-cy="abTesting-app-variant-2-vote-button-1"]').click()
+ cy.get('[data-cy="abTesting-both-bad-vote-button-2"]').click()
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/ai-critic-evaluation.cy.ts b/agenta-web/cypress/e2e/ai-critic-evaluation.cy.ts
new file mode 100644
index 0000000000..01f64f965c
--- /dev/null
+++ b/agenta-web/cypress/e2e/ai-critic-evaluation.cy.ts
@@ -0,0 +1,102 @@
+describe("AI Critics Evaluation workflow", () => {
+ let app_id
+ let testset_name
+ before(() => {
+ cy.createVariantsAndTestsets()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ cy.get("@testsetName").then((testsetName) => {
+ testset_name = testsetName
+ })
+ })
+
+ context("When you select evaluation without an API key", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.clearLocalStorage("openAiToken")
+
+ cy.get('[data-cy="evaluation-error-modal"]').should("not.exist")
+ cy.get('[data-cy="ai-critic-button"]').click()
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get('[data-cy^="testset"]').contains(testset_name).click()
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ cy.get('[data-cy="evaluation-error-modal"]').should("exist")
+ })
+
+ it("Should display when starting evaluation", () => {
+ cy.get('[data-cy="evaluation-error-modal-ok-button"]').click()
+ })
+
+ it("Should navigate to Settings when clicking on the modal", () => {
+ cy.get('[data-cy="evaluation-error-modal-nav-button"]').click()
+ cy.url().should("include", "/settings")
+ })
+ })
+
+ context("When you select evaluation with an API key", () => {
+ beforeEach(() => {
+ cy.addingOpenaiKey()
+
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.get('[data-cy="ai-critic-button"]').click()
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get('[data-cy^="testset"]').contains(testset_name).click()
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ })
+
+ it("Should successfully navigate to AI Critic", () => {
+ cy.get('[data-cy="evaluation-error-modal"]').should("not.exist")
+ cy.url().should("include", "/auto_ai_critique")
+ })
+
+ it("Should complete the evaluation workflow without errors", () => {
+ cy.get('[data-cy="ai-critic-evaluation-result"]').should(
+ "contain.text",
+ "Run evaluation to see results!",
+ )
+ cy.get(".ant-message-notice-content").should("not.exist")
+ cy.wait(1500)
+ cy.clickLinkAndWait('[data-cy="ai-critic-run-evaluation"]')
+ cy.get(".ant-spin").should("exist")
+
+ cy.get('[data-cy="ai-critic-evaluation-result"]').should("contain.text", "Results Data")
+
+ cy.get(".ant-spin").should("not.exist")
+ cy.get(".ant-message-notice-content").should("contain.text", "Evaluation Results Saved")
+ })
+
+ it("Should execute evaluation workflow with error", () => {
+ cy.clearLocalStorage("openAiToken")
+ cy.wait(1000)
+ cy.clickLinkAndWait('[data-cy="ai-critic-run-evaluation"]')
+
+ cy.get(".ant-spin").should("exist")
+ cy.get('[data-cy="ai-critic-evaluation-result"]').should(
+ "contain.text",
+ "Failed to run evaluation",
+ )
+
+ cy.get(".ant-spin").should("not.exist")
+ cy.get(".ant-message-notice-content").should("exist")
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/app-navigation.cy.ts b/agenta-web/cypress/e2e/app-navigation.cy.ts
new file mode 100644
index 0000000000..7b29ea0324
--- /dev/null
+++ b/agenta-web/cypress/e2e/app-navigation.cy.ts
@@ -0,0 +1,62 @@
+import {isDemo} from "../support/commands/utils"
+
+describe("App Navigation without errors", () => {
+ let app_id
+ before(() => {
+ cy.createVariant()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ })
+
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/playground`)
+ cy.contains(/modify parameters/i)
+ })
+
+ it("should navigate successfully to Playground", () => {
+ cy.location("pathname").should("include", "/playground")
+ cy.get('[data-cy="playground-header"]').within(() => {
+ cy.get("h2").should("contain.text", "1. Modify Parameters")
+ cy.get("button").should("have.length", 3)
+ })
+ })
+
+ it("should navigate successfully to Testsets", () => {
+ cy.clickLinkAndWait('[data-cy="app-testsets-link"]')
+ cy.location("pathname").should("include", "/testsets")
+ cy.get('[data-cy="app-testset-list"]').should("exist")
+ })
+
+ it("should navigate successfully to Evaluations", () => {
+ cy.clickLinkAndWait('[data-cy="app-evaluations-link"]')
+ cy.location("pathname").should("include", "/evaluations")
+ cy.get('[data-cy="evaluations-container"]').within(() => {
+ cy.contains("1. Select an evaluation type")
+ cy.contains("2. Which variants would you like to evaluate")
+ cy.contains("3. Which testset you want to use?")
+ })
+ })
+
+ if (isDemo()) {
+ it("should navigate successfully to Endpoints", () => {
+ cy.clickLinkAndWait('[data-cy="app-endpoints-link"]')
+ cy.location("pathname").should("include", "/endpoints")
+ cy.get('[data-cy="endpoints"]').within(() => {
+ cy.contains("API endpoint")
+ })
+ })
+ }
+
+ it("should navigate successfully to Settings", () => {
+ cy.clickLinkAndWait('[data-cy="settings-link"]')
+ cy.location("pathname").should("include", "/settings")
+ cy.get('[data-cy="secrets"]').within(() => {
+ cy.contains("LLM Keys")
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/code-evaluation.cy.ts b/agenta-web/cypress/e2e/code-evaluation.cy.ts
new file mode 100644
index 0000000000..58b1499499
--- /dev/null
+++ b/agenta-web/cypress/e2e/code-evaluation.cy.ts
@@ -0,0 +1,84 @@
+import {randString} from "../../src/lib/helpers/utils"
+
+// This is added to prevent Cypress from failing the test prematurely due to application errors.
+Cypress.on("uncaught:exception", () => false)
+
+describe("Code Evaluation workflow", () => {
+ const eval_name = randString(5)
+ let app_id
+ let testset_name
+ before(() => {
+ cy.createVariantsAndTestsets()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ cy.get("@testsetName").then((testsetName) => {
+ testset_name = testsetName
+ })
+ })
+
+ context("When navigating to Evaluation Page", () => {
+ it("Should reach the Evaluation Page", () => {
+ cy.visit(`/apps/${app_id}/playground`)
+ cy.contains(/modify parameters/i)
+ cy.clickLinkAndWait('[data-cy="app-evaluations-link"]')
+ cy.url().should("include", "/evaluations")
+ })
+ })
+
+ context("When creating a new evaluation", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.clickLinkAndWait('[data-cy="code-evaluation-button"]')
+ cy.clickLinkAndWait('[data-cy="new-code-evaluation-button"]')
+ cy.url().should("include", "/create_custom_evaluation")
+ })
+
+ it("Should add a new Code Evaluation successfully", () => {
+ cy.get('[data-cy="code-evaluation-save-button"]').should("be.disabled")
+ cy.get('[data-cy="code-evaluation-input"]').type(eval_name)
+ cy.get(".monaco-editor").type(".")
+ cy.get('[data-cy="code-evaluation-save-button"]').should("not.be.disabled")
+ cy.clickLinkAndWait('[data-cy="code-evaluation-save-button"]')
+ cy.url().should("include", "/evaluations")
+ })
+ })
+
+ context("When executing the evaluation", () => {
+ it("Should execute evaluation workflow successfully", () => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.clickLinkAndWait('[data-cy="code-evaluation-button"]')
+ cy.get('[data-cy^="code-evaluation-option"]').contains(eval_name).click()
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get('[data-cy^="testset"]').contains(testset_name).click()
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ cy.url().should("include", "/custom_code_run")
+ cy.wait(1500)
+ cy.clickLinkAndWait('[data-cy="code-evaluation-run"]')
+ cy.get(".ant-spin").should("exist")
+ })
+ })
+
+ context("When displaying Code Evaluation result", () => {
+ it("Should display Code Evaluation result", () => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.url().should("include", "/evaluations")
+ cy.get('[data-cy="automatic-evaluation-result"]').within(() => {
+ cy.get("tr").last().should("contain.text", "Custom Code Run")
+ })
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/exact-match-evaluation.cy.ts b/agenta-web/cypress/e2e/exact-match-evaluation.cy.ts
new file mode 100644
index 0000000000..24812a6b32
--- /dev/null
+++ b/agenta-web/cypress/e2e/exact-match-evaluation.cy.ts
@@ -0,0 +1,67 @@
+describe("Exact Match Evaluation workflow", () => {
+ let app_id
+ let testset_name
+ before(() => {
+ cy.createVariantsAndTestsets()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ cy.get("@testsetName").then((testsetName) => {
+ testset_name = testsetName
+ })
+ })
+
+ context("When navigating to Evaluation Page", () => {
+ it("Should reach the Evaluation Page", () => {
+ cy.visit(`/apps/${app_id}/playground`)
+ cy.contains(/modify parameters/i)
+ cy.clickLinkAndWait('[data-cy="app-evaluations-link"]')
+ cy.url().should("include", "/evaluations")
+ })
+ })
+
+ context("When executing the evaluation", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.url().should("include", "/evaluations")
+ })
+
+ it("Should execute evaluation workflow successfully", () => {
+ cy.get('[data-cy="exact-match-button"]').click()
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get('[data-cy^="testset"]').contains(testset_name).click()
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+
+ cy.url().should("include", "/auto_exact_match")
+ cy.wait(1500)
+ cy.get('[data-cy="exact-match-evaluation-button"]').click()
+
+ cy.get('[data-cy="exact-match-evaluation-score"]')
+ .invoke("text")
+ .then((text) => {
+ // Check if the text contains either "correct" or "wrong"
+ expect(text.includes("correct") || text.includes("wrong")).to.be.true
+ })
+
+ cy.get(".ant-statistic-content-value").first().should("contain", "3 out of 3")
+ cy.get(".ant-message-notice-content").should("exist")
+ })
+
+ it("Should display Exact Match Evaluation result", () => {
+ cy.get('[data-cy="automatic-evaluation-result"]').within(() => {
+ cy.get("tr").last().should("contain.text", "Exact Match")
+ })
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/playground.cy.ts b/agenta-web/cypress/e2e/playground.cy.ts
new file mode 100644
index 0000000000..da8a2aad3a
--- /dev/null
+++ b/agenta-web/cypress/e2e/playground.cy.ts
@@ -0,0 +1,20 @@
+describe("Playground Prompt Test", function () {
+ before(() => {
+ cy.createVariant()
+ })
+
+ it("Should test prompt functionality in the Playground", () => {
+ cy.get('[data-cy="testview-input-parameters-0"]').type("Germany")
+ cy.get('[data-cy="testview-input-parameters-run-button"]').click()
+ cy.get('[data-cy="testview-input-parameters-result"]').should("contain.text", "Loading...")
+ cy.get('[data-cy="testview-input-parameters-result"]').should(
+ "contain.text",
+ "The capital of Germany is Berlin.",
+ )
+ cy.get(".ant-message-notice-content").should("not.exist")
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/regex-evaluation.cy.ts b/agenta-web/cypress/e2e/regex-evaluation.cy.ts
new file mode 100644
index 0000000000..7c65917845
--- /dev/null
+++ b/agenta-web/cypress/e2e/regex-evaluation.cy.ts
@@ -0,0 +1,105 @@
+describe("Regex Evaluation workflow", () => {
+ let app_id
+ let testset_name
+ before(() => {
+ cy.createVariantsAndTestsets()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
+ cy.get("@testsetName").then((testsetName) => {
+ testset_name = testsetName
+ })
+ })
+
+ context("When navigating to Evaluation Page", () => {
+ it("Should reach the Evaluation Page", () => {
+ cy.visit(`/apps/${app_id}/playground`)
+ cy.contains(/modify parameters/i)
+ cy.clickLinkAndWait('[data-cy="app-evaluations-link"]')
+ cy.url().should("include", "/evaluations")
+ })
+ })
+
+ context("When no Variant and Testset are Selected", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ })
+
+ it("Should display a warning to select Variant", () => {
+ cy.clickLinkAndWait('[data-cy="regex-button"]')
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ cy.get(".ant-message-notice-content")
+ .should("contain.text", "Please select a variant")
+ .should("exist")
+ })
+
+ it("Should display a warning to select Testset", () => {
+ cy.clickLinkAndWait('[data-cy="regex-button"]')
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+ cy.get(".ant-message-notice-content")
+ .should("contain.text", "Please select a testset")
+ .should("exist")
+ })
+ })
+
+ context("When Variant and Testset are Selected", () => {
+ beforeEach(() => {
+ cy.visit(`/apps/${app_id}/evaluations`)
+ cy.clickLinkAndWait('[data-cy="regex-button"]')
+
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseover")
+ cy.get('[data-cy="variant-0"]').click()
+ cy.get('[data-cy="variants-dropdown-0"]').trigger("mouseout")
+
+ cy.get('[data-cy="selected-testset"]').trigger("mouseover")
+ cy.get('[data-cy^="testset"]').contains(testset_name).click()
+ cy.get('[data-cy="selected-testset"]').trigger("mouseout")
+
+ cy.clickLinkAndWait('[data-cy="start-new-evaluation-button"]')
+
+ cy.location("pathname").should("include", "/auto_regex_test")
+
+ cy.get(".ant-form-item-explain-error").should("not.exist")
+ })
+
+ it("Should display error for missing regex pattern", () => {
+ cy.clickLinkAndWait('[data-cy="regex-evaluation-run"]')
+
+ cy.get(".ant-form-item-explain-error").should("exist")
+ })
+
+ it("Should execute evaluation workflow successfully", () => {
+ cy.get('[data-cy="regex-evaluation-input"]').type(`^[A-Z][a-z]*$`)
+
+ cy.get('[data-cy="regex-evaluation-strategy"]').within(() => {
+ cy.get("label").eq(0).click()
+ })
+
+ cy.clickLinkAndWait('[data-cy="regex-evaluation-run"]')
+
+ cy.get('[data-cy="regex-evaluation-regex-match"]', {timeout: 60000})
+ .invoke("text")
+ .then((text) => {
+ // Check if the text contains either "Match" or "Mismatch"
+ expect(text.includes("Match") || text.includes("Mismatch")).to.be.true
+ })
+ cy.get('[data-cy="regex-evaluation-score"]', {timeout: 60000})
+ .invoke("text")
+ .then((text) => {
+ // Check if the text contains either "correct" or "wrong"
+ expect(text.includes("correct") || text.includes("wrong")).to.be.true
+ })
+
+ cy.get(".ant-message-notice-content").should("exist")
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
+ })
+})
diff --git a/agenta-web/cypress/e2e/smoke-tests.cy.ts b/agenta-web/cypress/e2e/smoke-tests.cy.ts
new file mode 100644
index 0000000000..e833ce5c8d
--- /dev/null
+++ b/agenta-web/cypress/e2e/smoke-tests.cy.ts
@@ -0,0 +1,18 @@
+describe("Basic smoke tests to see if app has loaded correctly", () => {
+ beforeEach(() => {
+ cy.visit("/apps")
+ })
+
+ it("should navigate successfully to the app page", () => {
+ cy.location("pathname").should("include", "/apps")
+ cy.contains("Apps").should("be.visible")
+ })
+
+ it("should navigate successfully to Settings", () => {
+ cy.clickLinkAndWait('[data-cy="settings-link"]')
+ cy.location("pathname").should("include", "/settings")
+ cy.get('[data-cy="secrets"]').within(() => {
+ cy.contains("LLM Keys")
+ })
+ })
+})
diff --git a/agenta-web/cypress/e2e/testset.cy.ts b/agenta-web/cypress/e2e/testset.cy.ts
index 64b4dd3c0d..14f7e10aa5 100644
--- a/agenta-web/cypress/e2e/testset.cy.ts
+++ b/agenta-web/cypress/e2e/testset.cy.ts
@@ -1,42 +1,74 @@
import {randString} from "../../src/lib/helpers/utils"
-describe("create a new testset", () => {
- beforeEach(() => {
- // navigate to the new testset page
- cy.visit("/apps")
- cy.clickLinkAndWait('[data-cy="app-card-link"]')
- cy.clickLinkAndWait('[data-cy="app-testsets-link"]')
- cy.clickLinkAndWait('[data-cy="testset-new-manual-link"]')
- })
+const countries = [
+ {country: "France", capital: "Paris"},
+ {country: "Germany", capital: "Berlin"},
+ {country: "Sweden", capital: "Stockholm"},
+]
- it("navigates successfully to the new testset page", () => {
- cy.url().should("include", "/testsets/new/manual")
+describe("Testsets crud and UI functionality", () => {
+ let app_id
+ before(() => {
+ cy.createVariant()
+ cy.get("@app_id").then((appId) => {
+ app_id = appId
+ })
})
- it("don't allow creation of a testset without a name", () => {
- cy.get('[data-cy="testset-save-button"]').click()
- cy.get('[data-cy="testset-name-reqd-error"]').should("be.visible")
- })
+ context("Testing creation process of testset", () => {
+ beforeEach(() => {
+ // navigate to the new testset page
+ cy.visit(`/apps/${app_id}/testsets/new/manual`)
+ })
+
+ it("navigates successfully to the new testset page", () => {
+ cy.url().should("include", "/testsets/new/manual")
+ })
+
+ it("don't allow creation of a testset without a name", () => {
+ cy.get('[data-cy="testset-save-button"]').click()
+ cy.get('[data-cy="testset-name-reqd-error"]').should("be.visible")
+ })
- it("successfully creates the testset and navigates to the list", () => {
- const testsetName = randString(8)
- cy.get('[data-cy="testset-name-input"]').type(testsetName)
- cy.get('[data-cy="testset-save-button"]').click()
- cy.url().should("include", "/testsets")
-
- // validate that the new testset is in the list
- cy.get('[data-cy="app-testset-list"]').as("table")
- cy.get("@table").get(".ant-table-pagination li a").last().click()
- cy.get("@table").contains(testsetName).as("tempTestSet").should("be.visible")
-
- //cleanup
- cy.get("@tempTestSet")
- .parent()
- .invoke("attr", "data-row-key")
- .then((id) => {
- cy.request("DELETE", `${Cypress.env().baseApiURL}/testsets/`, {
- testset_ids: [id],
- })
+ it("successfully creates the testset and navigates to the list", () => {
+ const testsetName = randString(8)
+ cy.get('[data-cy="testset-name-input"]').type(testsetName)
+ cy.get(".ag-row").should("have.length", 3)
+ countries.forEach((country, index) => {
+ cy.get(".ag-row")
+ .eq(index)
+ .within(() => {
+ cy.get("div.ag-cell")
+ .eq(1)
+ .within(() => {
+ cy.get("span").eq(0).dblclick()
+ cy.get(".ag-input-field-input").type(country.country)
+ })
+ cy.get("div.ag-cell")
+ .eq(2)
+ .within(() => {
+ cy.get("span").eq(0).dblclick()
+ cy.get(".ag-input-field-input").type(
+ `The capital of ${country.country} is ${country.capital}.`,
+ )
+ })
+ })
})
+ cy.intercept("/api/testsets/*").as("saveTestsetRequest")
+ cy.get('[data-cy="testset-save-button"]').click()
+ //wait for the save api to complete
+ cy.wait("@saveTestsetRequest")
+ cy.clickLinkAndWait('[data-cy="app-testsets-link"]')
+ cy.url().should("include", "/testsets")
+
+ // validate that the new testset is in the list
+ cy.get('[data-cy="app-testset-list"]').as("table")
+ cy.get("@table").get(".ant-table-pagination li a").last().click()
+ cy.get("@table").contains(testsetName).as("tempTestSet").should("be.visible")
+ })
+ })
+
+ after(() => {
+ cy.cleanupVariantAndTestset()
})
})
diff --git a/agenta-web/cypress/support/commands/evaluations.ts b/agenta-web/cypress/support/commands/evaluations.ts
new file mode 100644
index 0000000000..38c859310d
--- /dev/null
+++ b/agenta-web/cypress/support/commands/evaluations.ts
@@ -0,0 +1,112 @@
+import {randString, removeOpenAIKey} from "../../../src/lib/helpers/utils"
+
+let app_id
+
+const countries = [
+ {country: "France", capital: "Paris"},
+ {country: "Germany", capital: "Berlin"},
+ {country: "Sweden", capital: "Stockholm"},
+]
+
+const apiKey = Cypress.env("NEXT_PUBLIC_OPENAI_API_KEY")
+
+Cypress.Commands.add("createVariant", () => {
+ cy.addingOpenaiKey()
+ cy.visit("/apps")
+
+ // Check if there are app variants present
+ cy.request({
+ url: `${Cypress.env().baseApiURL}/organizations/`,
+ method: "GET",
+ }).then((res) => {
+ cy.log(`Body: ${JSON.stringify(res.body) || "No body"}`)
+ cy.request({
+ url: `${Cypress.env().baseApiURL}/apps/?org_id=${res.body[0]?.id}`,
+ method: "GET",
+ }).then((resp) => {
+ if (resp.body.length) {
+ cy.get('[data-cy="create-new-app-button"]').click()
+ cy.get('[data-cy="create-from-template"]').click()
+ } else {
+ cy.get('[data-cy="create-from-template__no-app"]').click()
+ }
+ })
+ })
+
+ cy.get('[data-cy="create-app-button"]').first().click()
+ const appName = randString(5)
+
+ cy.get('[data-cy="enter-app-name-modal"]')
+ .should("exist")
+ .within(() => {
+ cy.get("input").type(appName)
+ })
+
+ cy.get('[data-cy="enter-app-name-modal-button"]').click()
+
+ cy.url().should("include", "/playground")
+ cy.url().then((url) => {
+ app_id = url.match(/\/apps\/([a-zA-Z0-9]+)\/playground/)[1]
+
+ cy.wrap(app_id).as("app_id")
+ })
+ cy.contains(/modify parameters/i)
+ cy.removeOpenAiKey()
+})
+
+Cypress.Commands.add("createVariantsAndTestsets", () => {
+ cy.createVariant()
+
+ cy.clickLinkAndWait('[data-cy="app-testsets-link"]')
+ cy.clickLinkAndWait('[data-cy="testset-new-manual-link"]')
+ const testsetName = randString(5)
+
+ cy.get('[data-cy="testset-name-input"]').type(testsetName)
+ cy.wrap(testsetName).as("testsetName")
+
+ cy.get(".ag-row").should("have.length", 3)
+ countries.forEach((country, index) => {
+ cy.get(".ag-row")
+ .eq(index)
+ .within(() => {
+ cy.get("div.ag-cell")
+ .eq(1)
+ .within(() => {
+ cy.get("span").eq(0).dblclick()
+ cy.get(".ag-input-field-input").type(country.country)
+ })
+ cy.get("div.ag-cell")
+ .eq(2)
+ .within(() => {
+ cy.get("span").eq(0).dblclick()
+ cy.get(".ag-input-field-input").type(
+ `The capital of ${country.country} is ${country.capital}.`,
+ )
+ })
+ })
+ })
+
+ cy.get('[data-cy="testset-save-button"]').click()
+})
+
+Cypress.Commands.add("cleanupVariantAndTestset", () => {
+ cy.request({
+ url: `${Cypress.env().baseApiURL}/apps/${app_id}/`,
+ method: "DELETE",
+ body: {
+ app_id,
+ },
+ })
+
+ cy.removeOpenAiKey()
+})
+
+Cypress.Commands.add("addingOpenaiKey", () => {
+ cy.visit("/settings")
+ cy.get('[data-cy="openai-api-input"]').type(apiKey)
+ cy.get('[data-cy="openai-api-save"]').click()
+})
+
+Cypress.Commands.add("removeOpenAiKey", () => {
+ removeOpenAIKey()
+})
diff --git a/agenta-web/cypress/support/commands/index.ts b/agenta-web/cypress/support/commands/index.ts
index aab1817cbe..83e4041295 100644
--- a/agenta-web/cypress/support/commands/index.ts
+++ b/agenta-web/cypress/support/commands/index.ts
@@ -1,11 +1,18 @@
///
import "./utils"
+import "./evaluations"
declare global {
namespace Cypress {
interface Chainable {
clickLinkAndWait(selector: string): Chainable
+ createVariantsAndTestsets(): Chainable
+ cleanupVariantAndTestset(): Chainable
+ createVariant(): Chainable
+ saveOpenAiKey(): Chainable
+ removeOpenAiKey(): Chainable
+ addingOpenaiKey(): Chainable
}
}
}
diff --git a/agenta-web/cypress/support/commands/utils.ts b/agenta-web/cypress/support/commands/utils.ts
index 8153683cc9..8f7bd5b3fc 100644
--- a/agenta-web/cypress/support/commands/utils.ts
+++ b/agenta-web/cypress/support/commands/utils.ts
@@ -2,3 +2,10 @@ Cypress.Commands.add("clickLinkAndWait", (selector) => {
cy.get(selector).first().as("link")
cy.get("@link").click()
})
+
+export const isDemo = () => {
+ if (Cypress.env["NEXT_PUBLIC_FF"]) {
+ return ["cloud", "ee"].includes(Cypress.env["NEXT_PUBLIC_FF"])
+ }
+ return false
+}
diff --git a/agenta-web/dev.Dockerfile b/agenta-web/dev.Dockerfile
index deabaea702..6f86dbd847 100644
--- a/agenta-web/dev.Dockerfile
+++ b/agenta-web/dev.Dockerfile
@@ -6,7 +6,7 @@ WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
- elif [ -f package-lock.json ]; then npm ci; \
+ elif [ -f package-lock.json ]; then npm i; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
# Allow install without lockfile, so example works even without Node.js installed locally
else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \
@@ -17,6 +17,10 @@ COPY public ./public
COPY next.config.js .
COPY tsconfig.json .
COPY postcss.config.js .
+COPY .env .
+RUN if [ -f .env.local ]; then cp .env.local .; fi
+# used in cloud
+COPY sentry.* .
# Next.js collects completely anonymous telemetry data about general usage. Learn more here: https://nextjs.org/telemetry
# Uncomment the following line to disable telemetry at run time
# ENV NEXT_TELEMETRY_DISABLED 1
diff --git a/agenta-web/next.config.js b/agenta-web/next.config.js
index cdff80ecc5..39085a0456 100644
--- a/agenta-web/next.config.js
+++ b/agenta-web/next.config.js
@@ -11,6 +11,7 @@ const nextConfig = {
output: "standalone",
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
+ productionBrowserSourceMaps: true,
async redirects() {
return [
@@ -21,6 +22,26 @@ const nextConfig = {
},
]
},
+
+ webpack: (config, {webpack, isServer}) => {
+ const envs = {}
+
+ Object.keys(process.env).forEach((env) => {
+ if (env.startsWith("NEXT_PUBLIC_")) {
+ envs[env] = process.env[env]
+ }
+ })
+
+ if (!isServer) {
+ config.plugins.push(
+ new webpack.DefinePlugin({
+ "process.env": JSON.stringify(envs),
+ }),
+ )
+ }
+
+ return config
+ },
}
module.exports = withMDX(nextConfig)
diff --git a/agenta-web/package-lock.json b/agenta-web/package-lock.json
index 57f1b4b3b0..efaacbff63 100644
--- a/agenta-web/package-lock.json
+++ b/agenta-web/package-lock.json
@@ -10,38 +10,55 @@
"dependencies": {
"@ant-design/colors": "^7.0.0",
"@ant-design/icons": "^5.0.1",
+ "@dnd-kit/core": "^6.1.0",
+ "@dnd-kit/sortable": "^8.0.0",
"@heroicons/react": "^2.0.17",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
+ "@monaco-editor/react": "^4.5.2",
"@next/mdx": "^13.4.13",
+ "@sentry/nextjs": "^7.77.0",
+ "@types/js-beautify": "^1.14.0",
+ "@types/lodash": "^4.14.201",
"@types/mdx": "^2.0.6",
- "@types/node": "18.16.3",
- "@types/react": "18.2.0",
"@types/react-dom": "18.2.1",
+ "@types/react-highlight-words": "^0.16.4",
+ "@types/react-syntax-highlighter": "^15.5.7",
+ "@types/uuid": "^9.0.7",
"ag-grid-community": "^30.0.6",
"ag-grid-react": "^30.0.3",
"antd": "^5.4.7",
"autoprefixer": "10.4.14",
"axios": "^1.4.0",
"classnames": "^2.3.2",
+ "dotenv": "^16.3.1",
"eslint": "8.39.0",
"eslint-config-next": "13.3.4",
+ "jotai": "^2.5.0",
"js-beautify": "^1.14.8",
+ "lodash": "^4.17.21",
"next": "13.3.4",
+ "nextjs-cors": "^2.1.2",
"postcss": "8.4.23",
+ "posthog-js": "^1.87.2",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-error-boundary": "^4.0.11",
+ "react-highlight-words": "^0.20.0",
"react-jss": "^10.10.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-youtube": "^10.1.0",
+ "supertokens-auth-react": "^0.34.0",
+ "supertokens-node": "^15.0.4",
"swr": "^2.1.5",
"typescript": "5.0.4",
- "usehooks-ts": "^2.9.1"
+ "usehooks-ts": "^2.9.1",
+ "uuid": "^9.0.1"
},
"devDependencies": {
- "cypress": "^12.11.0",
- "eslint-config-prettier": "^8.8.0",
+ "@types/node": "^20.8.10",
+ "cypress": "^13.4.0",
"node-mocks-http": "^1.12.2",
"prettier": "^3.0.0"
}
@@ -63,16 +80,16 @@
}
},
"node_modules/@ant-design/cssinjs": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.10.1.tgz",
- "integrity": "sha512-PSoJS8RMzn95ZRg007dJGr6AU0Zim/O+tTN0xmXmh9CkIl4y3wuOr2Zhehaj7s130wPSYDVvahf3DKT50w/Zhw==",
+ "version": "1.17.2",
+ "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.17.2.tgz",
+ "integrity": "sha512-vu7lnfEx4Mf8MPzZxn506Zen3Nt4fRr2uutwvdCuTCN5IiU0lDdQ0tiJ24/rmB8+pefwjluYsbyzbQSbgfJy+A==",
"dependencies": {
"@babel/runtime": "^7.11.1",
"@emotion/hash": "^0.8.0",
"@emotion/unitless": "^0.7.5",
"classnames": "^2.3.1",
"csstype": "^3.0.10",
- "rc-util": "^5.27.0",
+ "rc-util": "^5.35.0",
"stylis": "^4.0.13"
},
"peerDependencies": {
@@ -81,12 +98,12 @@
}
},
"node_modules/@ant-design/icons": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.1.4.tgz",
- "integrity": "sha512-YHKL7Jx3bM12OxvtiYDon04BsBT/6LGitYEqar3GljzWaAyMOAD8i/uF1Rsi5Us/YNdWWXBGSvZV2OZWMpJlcA==",
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.2.6.tgz",
+ "integrity": "sha512-4wn0WShF43TrggskBJPRqCD0fcHbzTYjnaoskdiJrVHg86yxoZ8ZUqsXvyn4WUqehRiFKnaclOhqk9w4Ui2KVw==",
"dependencies": {
"@ant-design/colors": "^7.0.0",
- "@ant-design/icons-svg": "^4.2.1",
+ "@ant-design/icons-svg": "^4.3.0",
"@babel/runtime": "^7.11.2",
"classnames": "^2.2.6",
"rc-util": "^5.31.1"
@@ -100,14 +117,14 @@
}
},
"node_modules/@ant-design/icons-svg": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz",
- "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw=="
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.3.1.tgz",
+ "integrity": "sha512-4QBZg8ccyC6LPIRii7A0bZUk3+lEDCLnhB+FVsflGdcWPPmV+j3fire4AwwoqHV/BibgvBmR9ZIo4s867smv+g=="
},
"node_modules/@ant-design/react-slick": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.1.tgz",
- "integrity": "sha512-ARM0TmpGdDuUVE10NwUCENQlJSInNKo5NiBjL5szu5BxWNEHNwQMcDrlVCqFbkvFLy+2CvywW8Y59QJtC0YDag==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz",
+ "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==",
"dependencies": {
"@babel/runtime": "^7.10.4",
"classnames": "^2.2.5",
@@ -120,11 +137,11 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
- "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
+ "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
"dependencies": {
- "regenerator-runtime": "^0.13.11"
+ "regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@@ -141,17 +158,17 @@
}
},
"node_modules/@ctrl/tinycolor": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
- "integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==",
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"engines": {
"node": ">=10"
}
},
"node_modules/@cypress/request": {
- "version": "2.88.11",
- "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz",
- "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz",
+ "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==",
"dev": true,
"dependencies": {
"aws-sign2": "~0.7.0",
@@ -167,9 +184,9 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
- "qs": "~6.10.3",
+ "qs": "6.10.4",
"safe-buffer": "^5.1.2",
- "tough-cookie": "~2.5.0",
+ "tough-cookie": "^4.1.3",
"tunnel-agent": "^0.6.0",
"uuid": "^8.3.2"
},
@@ -191,6 +208,15 @@
"node": ">= 0.12"
}
},
+ "node_modules/@cypress/request/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/@cypress/xvfb": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
@@ -210,6 +236,55 @@
"ms": "^2.1.1"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
+ "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz",
+ "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
+ "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.1.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
@@ -248,21 +323,21 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
- "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/eslintrc": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
- "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
- "espree": "^9.5.2",
+ "espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -294,11 +369,11 @@
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.10",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
- "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dependencies": {
- "@humanwhocodes/object-schema": "^1.2.1",
+ "@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
@@ -319,9 +394,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
- "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw=="
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
@@ -368,13 +443,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
- "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
- "peer": true
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.19",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
- "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+ "version": "0.3.20",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
+ "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"peer": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -441,6 +515,30 @@
"react": ">=16"
}
},
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz",
+ "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.21.0 < 1"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz",
+ "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.4.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@next/env": {
"version": "13.3.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.3.4.tgz",
@@ -455,9 +553,9 @@
}
},
"node_modules/@next/mdx": {
- "version": "13.4.13",
- "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-13.4.13.tgz",
- "integrity": "sha512-rQHfeyPO6WC+mEqFCNibV8CWXBJ95uRfPv05Xb8ZMUr284grCIAaSfSRDDrwcoknizyfREDp33oV/isIujS4pA==",
+ "version": "13.5.6",
+ "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-13.5.6.tgz",
+ "integrity": "sha512-2AMyCrz1SxSWNUpADyLz3RbPbq0GHrchbO7Msvg7IsH8MrTw3VYaZSI1KNa6JzZIoykwtNVSEL+uBmPZi106Jw==",
"dependencies": {
"source-map": "^0.7.0"
},
@@ -474,6 +572,66 @@
}
}
},
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.4.tgz",
+ "integrity": "sha512-vux7RWfzxy1lD21CMwZsy9Ej+0+LZdIIj1gEhVmzOQqQZ5N56h8JamrjIVCfDL+Lpj8KwOmFZbPHE8qaYnL2pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.4.tgz",
+ "integrity": "sha512-1tb+6JT98+t7UIhVQpKL7zegKnCs9RKU6cKNyj+DYKuC/NVl49/JaIlmwCwK8Ibl+RXxJrK7uSXSIO71feXsgw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.4.tgz",
+ "integrity": "sha512-UqcKkYTKslf5YAJNtZ5XV1D5MQJIkVtDHL8OehDZERHzqOe7jvy41HFto33IDPPU8gJiP5eJb3V9U26uifqHjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.4.tgz",
+ "integrity": "sha512-HE/FmE8VvstAfehyo/XsrhGgz97cEr7uf9IfkgJ/unqSXE0CDshDn/4as6rRid74eDR8/exi7c2tdo49Tuqxrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.4.tgz",
@@ -504,6 +662,51 @@
"node": ">= 10"
}
},
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.4.tgz",
+ "integrity": "sha512-7dL+CAUAjmgnVbjXPIpdj7/AQKFqEUL3bKtaOIE1JzJ5UMHHAXCPwzQtibrsvQpf9MwcAmiv8aburD3xH1xf8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.4.tgz",
+ "integrity": "sha512-qplTyzEl1vPkS+/DRK3pKSL0HeXrPHkYsV7U6gboHYpfqoHY+bcLUj3gwVUa9PEHRIoq4vXvPzx/WtzE6q52ng==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.3.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.4.tgz",
+ "integrity": "sha512-usdvZT7JHrTuXC+4OKN5mCzUkviFkCyJJTkEz8jhBpucg+T7s83e7owm3oNFzmj5iKfvxU2St6VkcnSgpFvEYA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -536,29 +739,15 @@
"node": ">= 8"
}
},
- "node_modules/@pkgr/utils": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz",
- "integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "fast-glob": "^3.2.12",
- "is-glob": "^4.0.3",
- "open": "^9.1.0",
- "picocolors": "^1.0.0",
- "tslib": "^2.5.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/unts"
- }
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="
},
"node_modules/@rc-component/color-picker": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.2.0.tgz",
- "integrity": "sha512-IitJ6RWGHs7btI1AqzGPrehr5bueWLGDUyMKwDwvFunfSDo/o8g/95kUG55vC5EYLM0ZJ3SDfw45OrW5KAx3oA==",
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.4.1.tgz",
+ "integrity": "sha512-vh5EWqnsayZa/JwUznqDaPJz39jznx/YDbyBuVJntv735tKXKwEUZZb2jYEldOg+NKWZwtALjGMrNeGBmqFoEw==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@ctrl/tinycolor": "^3.6.0",
@@ -571,9 +760,9 @@
}
},
"node_modules/@rc-component/context": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.3.0.tgz",
- "integrity": "sha512-6QdaCJ7Wn5UZLJs15IEfqy4Ru3OaL5ctqpQYWd5rlfV9wwzrzdt6+kgAQZV/qdB0MUPN4nhyBfRembQCIvBf+w==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
+ "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"rc-util": "^5.27.0"
@@ -595,9 +784,9 @@
}
},
"node_modules/@rc-component/mutate-observer": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.0.0.tgz",
- "integrity": "sha512-okqRJSfNisXdI6CUeOLZC5ukBW/8kir2Ii4PJiKpUt+3+uS7dxwJUMxsUZquxA1rQuL8YcEmKVp/TCnR+yUdZA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
+ "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
"dependencies": {
"@babel/runtime": "^7.18.0",
"classnames": "^2.3.2",
@@ -612,9 +801,9 @@
}
},
"node_modules/@rc-component/portal": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.1.tgz",
- "integrity": "sha512-m8w3dFXX0H6UkJ4wtfrSwhe2/6M08uz24HHrF8pWfAXPwA9hwCuTE5per/C86KwNLouRpwFGcr7LfpHaa1F38g==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
+ "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
"dependencies": {
"@babel/runtime": "^7.18.0",
"classnames": "^2.3.2",
@@ -629,9 +818,9 @@
}
},
"node_modules/@rc-component/tour": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.8.0.tgz",
- "integrity": "sha512-rrRGioHTLQlGca27G2+lw7QpRb3uuMYCUIJjj31/B44VCJS0P2tqYhOgtzvWQmaLMlWH3ZlpzotkKX13NT4XEA==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.10.0.tgz",
+ "integrity": "sha512-voV0BKaTJbewB9LLgAHQ7tAGG7rgDkKQkZo82xw2gIk542hY+o7zwoqdN16oHhIKk7eG/xi+mdXrONT62Dt57A==",
"dependencies": {
"@babel/runtime": "^7.18.0",
"@rc-component/portal": "^1.0.0-9",
@@ -648,17 +837,16 @@
}
},
"node_modules/@rc-component/trigger": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.14.0.tgz",
- "integrity": "sha512-BZ1OnofveyVhoeAPQHbkvUM00N4C6cWkNR7JduoAd1sl9m+Gt7atXtlbtT+ovjjD7poQ0HPLfyGk0P0PZXPxhQ==",
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.18.0.tgz",
+ "integrity": "sha512-vloGnWpeTmt7DBw0OHnG9poQ8h1WFh0hebq6fpgVjGYSxm6JU8rLH+kNwVNNvhL6Rg5He4ESjOk6O7uF9dJhxA==",
"dependencies": {
- "@babel/runtime": "^7.18.3",
+ "@babel/runtime": "^7.23.2",
"@rc-component/portal": "^1.1.0",
"classnames": "^2.3.2",
- "rc-align": "^4.0.0",
"rc-motion": "^2.0.0",
"rc-resize-observer": "^1.3.1",
- "rc-util": "^5.33.0"
+ "rc-util": "^5.38.0"
},
"engines": {
"node": ">=8.x"
@@ -668,10 +856,330 @@
"react-dom": ">=16.9.0"
}
},
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "24.0.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz",
+ "integrity": "sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "glob": "^8.0.3",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz",
+ "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+ },
"node_modules/@rushstack/eslint-patch": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz",
- "integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw=="
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz",
+ "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA=="
+ },
+ "node_modules/@sentry-internal/tracing": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.77.0.tgz",
+ "integrity": "sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw==",
+ "dependencies": {
+ "@sentry/core": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/browser": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.77.0.tgz",
+ "integrity": "sha512-nJ2KDZD90H8jcPx9BysQLiQW+w7k7kISCWeRjrEMJzjtge32dmHA8G4stlUTRIQugy5F+73cOayWShceFP7QJQ==",
+ "dependencies": {
+ "@sentry-internal/tracing": "7.77.0",
+ "@sentry/core": "7.77.0",
+ "@sentry/replay": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/cli": {
+ "version": "1.75.2",
+ "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.75.2.tgz",
+ "integrity": "sha512-CG0CKH4VCKWzEaegouWfCLQt9SFN+AieFESCatJ7zSuJmzF05ywpMusjxqRul6lMwfUhRKjGKOzcRJ1jLsfTBw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "https-proxy-agent": "^5.0.0",
+ "mkdirp": "^0.5.5",
+ "node-fetch": "^2.6.7",
+ "progress": "^2.0.3",
+ "proxy-from-env": "^1.1.0",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "sentry-cli": "bin/sentry-cli"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.77.0.tgz",
+ "integrity": "sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==",
+ "dependencies": {
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/integrations": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.77.0.tgz",
+ "integrity": "sha512-P055qXgBHeZNKnnVEs5eZYLdy6P49Zr77A1aWJuNih/EenzMy922GOeGy2mF6XYrn1YJSjEwsNMNsQkcvMTK8Q==",
+ "dependencies": {
+ "@sentry/core": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0",
+ "localforage": "^1.8.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/nextjs": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-7.77.0.tgz",
+ "integrity": "sha512-8tYPBt5luFjrng1sAMJqNjM9sq80q0jbt6yariADU9hEr7Zk8YqFaOI2/Q6yn9dZ6XyytIRtLEo54kk2AO94xw==",
+ "dependencies": {
+ "@rollup/plugin-commonjs": "24.0.0",
+ "@sentry/core": "7.77.0",
+ "@sentry/integrations": "7.77.0",
+ "@sentry/node": "7.77.0",
+ "@sentry/react": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0",
+ "@sentry/vercel-edge": "7.77.0",
+ "@sentry/webpack-plugin": "1.20.0",
+ "chalk": "3.0.0",
+ "resolve": "1.22.8",
+ "rollup": "2.78.0",
+ "stacktrace-parser": "^0.1.10"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "next": "^10.0.8 || ^11.0 || ^12.0 || ^13.0 || ^14.0",
+ "react": "16.x || 17.x || 18.x",
+ "webpack": ">= 4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sentry/nextjs/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/nextjs/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.77.0.tgz",
+ "integrity": "sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g==",
+ "dependencies": {
+ "@sentry-internal/tracing": "7.77.0",
+ "@sentry/core": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0",
+ "https-proxy-agent": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/react": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.77.0.tgz",
+ "integrity": "sha512-Q+htKzib5em0MdaQZMmPomaswaU3xhcVqmLi2CxqQypSjbYgBPPd+DuhrXKoWYLDDkkbY2uyfe4Lp3yLRWeXYw==",
+ "dependencies": {
+ "@sentry/browser": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": "15.x || 16.x || 17.x || 18.x"
+ }
+ },
+ "node_modules/@sentry/replay": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.77.0.tgz",
+ "integrity": "sha512-M9Ik2J5ekl+C1Och3wzLRZVaRGK33BlnBwfwf3qKjgLDwfKW+1YkwDfTHbc2b74RowkJbOVNcp4m8ptlehlSaQ==",
+ "dependencies": {
+ "@sentry-internal/tracing": "7.77.0",
+ "@sentry/core": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@sentry/types": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.77.0.tgz",
+ "integrity": "sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/utils": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.77.0.tgz",
+ "integrity": "sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==",
+ "dependencies": {
+ "@sentry/types": "7.77.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/vercel-edge": {
+ "version": "7.77.0",
+ "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-7.77.0.tgz",
+ "integrity": "sha512-ffddPCgxVeAccPYuH5sooZeHBqDuJ9OIhIRYKoDi4TvmwAzWo58zzZWhRpkHqHgIQdQvhLVZ5F+FSQVWnYSOkw==",
+ "dependencies": {
+ "@sentry/core": "7.77.0",
+ "@sentry/types": "7.77.0",
+ "@sentry/utils": "7.77.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@sentry/webpack-plugin": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz",
+ "integrity": "sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw==",
+ "dependencies": {
+ "@sentry/cli": "^1.74.6",
+ "webpack-sources": "^2.0.0 || ^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
},
"node_modules/@swc/helpers": {
"version": "0.5.1",
@@ -690,17 +1198,17 @@
}
},
"node_modules/@types/debug": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
- "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.10.tgz",
+ "integrity": "sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/eslint": {
- "version": "8.44.2",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
- "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==",
+ "version": "8.44.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz",
+ "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==",
"peer": true,
"dependencies": {
"@types/estree": "*",
@@ -708,9 +1216,9 @@
}
},
"node_modules/@types/eslint-scope": {
- "version": "3.7.4",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
- "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
+ "version": "3.7.6",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz",
+ "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==",
"peer": true,
"dependencies": {
"@types/eslint": "*",
@@ -718,30 +1226,35 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
- "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz",
+ "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw=="
},
"node_modules/@types/estree-jsx": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.0.tgz",
- "integrity": "sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.2.tgz",
+ "integrity": "sha512-GNBWlGBMjiiiL5TSkvPtOteuXsiVitw5MYGY1UYlrAq0SKyczsls6sCD7TZ8fsjRsvCVxml7EbyjJezPb3DrSA==",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@types/hast": {
- "version": "2.3.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
- "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.7.tgz",
+ "integrity": "sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw==",
"dependencies": {
- "@types/unist": "*"
+ "@types/unist": "^2"
}
},
+ "node_modules/@types/js-beautify": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/@types/js-beautify/-/js-beautify-1.14.2.tgz",
+ "integrity": "sha512-y8qPAY4s0MbnYM/5taN+chNEdFmtqMBCGpjNGhZ6+wePkVneiWYRVSATm1+baccW8rABMCh7TmcocXjcTuKs0Q=="
+ },
"node_modules/@types/json-schema": {
- "version": "7.0.12",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
- "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
+ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
"peer": true
},
"node_modules/@types/json5": {
@@ -749,38 +1262,46 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
+ "node_modules/@types/lodash": {
+ "version": "4.14.201",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
+ "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
+ },
"node_modules/@types/mdast": {
- "version": "3.0.12",
- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz",
- "integrity": "sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg==",
+ "version": "3.0.14",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz",
+ "integrity": "sha512-gVZ04PGgw1qLZKsnWnyFv4ORnaJ+DXLdHTVSFbU8yX6xZ34Bjg4Q32yPkmveUP1yItXReKfB0Aknlh/3zxTKAw==",
"dependencies": {
"@types/unist": "^2"
}
},
"node_modules/@types/mdx": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.6.tgz",
- "integrity": "sha512-sVcwEG10aFU2KcM7cIA0M410UPv/DesOPyG8zMVk0QUDexHA3lYmGucpEpZ2dtWWhi2ip3CG+5g/iH0PwoW4Fw=="
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.9.tgz",
+ "integrity": "sha512-OKMdj17y8Cs+k1r0XFyp59ChSOwf8ODGtMQ4mnpfz5eFDk1aO41yN3pSKGuvVzmWAkFp37seubY1tzOVpwfWwg=="
},
"node_modules/@types/ms": {
- "version": "0.7.31",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
- "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
+ "version": "0.7.33",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.33.tgz",
+ "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
},
"node_modules/@types/node": {
- "version": "18.16.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz",
- "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q=="
+ "version": "20.8.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
+ "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/prop-types": {
- "version": "15.7.5",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
- "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ "version": "15.7.9",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
+ "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
},
"node_modules/@types/react": {
- "version": "18.2.0",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz",
- "integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==",
+ "version": "18.2.34",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.34.tgz",
+ "integrity": "sha512-U6eW/alrRk37FU/MS2RYMjx0Va2JGIVXELTODaTIYgvWGCV4Y4TfTUzG8DdmpDNIT0Xpj/R7GfyHOJJrDttcvg==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -795,10 +1316,26 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-highlight-words": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@types/react-highlight-words/-/react-highlight-words-0.16.6.tgz",
+ "integrity": "sha512-iGgne7Sf/vWfuOOWKURqj2LQwC8zBTZvNaw/uh9ZjwQfG40zfZKdkaE6387eWaDYjGdXQYPjUAhxjlaQhg4VYg==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-syntax-highlighter": {
+ "version": "15.5.9",
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.9.tgz",
+ "integrity": "sha512-ven8zRSVMNkqt8ySJ2eEW5ugbfl/V/Z9S1c9UNhGqwkwVZNV5akk10rYDALxgwS25cLmN+/Q5UxlGj9CJmZ6Ew==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/scheduler": {
- "version": "0.16.3",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz",
+ "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw=="
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -807,20 +1344,25 @@
"dev": true
},
"node_modules/@types/sizzle": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
- "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
+ "integrity": "sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ==",
"dev": true
},
"node_modules/@types/unist": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
- "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.9.tgz",
+ "integrity": "sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ=="
+ },
+ "node_modules/@types/uuid": {
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
+ "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g=="
},
"node_modules/@types/yauzl": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
- "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.2.tgz",
+ "integrity": "sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==",
"dev": true,
"optional": true,
"dependencies": {
@@ -828,13 +1370,13 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
- "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dependencies": {
- "@typescript-eslint/scope-manager": "5.60.1",
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/typescript-estree": "5.60.1",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
"debug": "^4.3.4"
},
"engines": {
@@ -854,12 +1396,12 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
- "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/visitor-keys": "5.60.1"
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -870,9 +1412,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
- "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -882,12 +1424,12 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
- "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
- "@typescript-eslint/visitor-keys": "5.60.1",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -908,11 +1450,11 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "5.60.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
- "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
"dependencies": {
- "@typescript-eslint/types": "5.60.1",
+ "@typescript-eslint/types": "5.62.0",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -1100,9 +1642,9 @@
}
},
"node_modules/acorn": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
- "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"bin": {
"acorn": "bin/acorn"
},
@@ -1128,23 +1670,34 @@
}
},
"node_modules/ag-grid-community": {
- "version": "30.0.6",
- "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-30.0.6.tgz",
- "integrity": "sha512-aMTJNXFDC00QxMaI0/V6M0GTzAtsXUvuxuld97S1Kqb4LvFNsycZlQ3/IcHW7JangRQZxAcwmSpBQBV/lcWoEg=="
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-30.2.0.tgz",
+ "integrity": "sha512-Gd6GXmtzEQSCDloBdRxxCDqnjTBRAOf/zzlaxxyyVBJgc+cePuNgGdplRUhT/rwIiDwvyuoynvxelVE/iYdXsA=="
},
"node_modules/ag-grid-react": {
- "version": "30.0.3",
- "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-30.0.3.tgz",
- "integrity": "sha512-4jhNxwHjF5dp1kgz57NGQ1JkfCSbVkiq75riKL2hFJAvVp1+FPz28H+FqZwz5VWn/rEvV5sC1H0RhnIdik0oRA==",
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-30.2.0.tgz",
+ "integrity": "sha512-y4esND0ADJMw/aCyfiT1GA885sy2XvlnXUNQdiXijoxcGY6OSk3jE2DPtZfE+RsdmNdXimMpoRZHwJW/aPCegA==",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
- "ag-grid-community": "~30.0.3",
+ "ag-grid-community": "~30.2.0",
"react": "^16.3.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -1241,56 +1794,56 @@
}
},
"node_modules/antd": {
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/antd/-/antd-5.6.3.tgz",
- "integrity": "sha512-SbZ1rX/eNXiSerF0V048wmh3tdTJdJpP4OsQS2sNH5f+WjZHgC92M7Qqp5YKOM4Rs8GYq63aePpk7SITl85Jqg==",
+ "version": "5.10.3",
+ "resolved": "https://registry.npmjs.org/antd/-/antd-5.10.3.tgz",
+ "integrity": "sha512-IV+F4P9Fm0pXj4WFCLVLKIu7eCtVKZMLvU1a0HUIRBEy5YPsD5bDzjYkZ4F+RVPaFrAjAvChrWcX6NtQOVNuJw==",
"dependencies": {
"@ant-design/colors": "^7.0.0",
- "@ant-design/cssinjs": "^1.10.1",
- "@ant-design/icons": "^5.1.0",
- "@ant-design/react-slick": "~1.0.0",
+ "@ant-design/cssinjs": "^1.17.2",
+ "@ant-design/icons": "^5.2.6",
+ "@ant-design/react-slick": "~1.0.2",
"@babel/runtime": "^7.18.3",
- "@ctrl/tinycolor": "^3.6.0",
- "@rc-component/color-picker": "~1.2.0",
- "@rc-component/mutate-observer": "^1.0.0",
- "@rc-component/tour": "~1.8.0",
- "@rc-component/trigger": "^1.13.0",
+ "@ctrl/tinycolor": "^3.6.1",
+ "@rc-component/color-picker": "~1.4.1",
+ "@rc-component/mutate-observer": "^1.1.0",
+ "@rc-component/tour": "~1.10.0",
+ "@rc-component/trigger": "^1.18.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"dayjs": "^1.11.1",
"qrcode.react": "^3.1.0",
- "rc-cascader": "~3.12.0",
+ "rc-cascader": "~3.18.1",
"rc-checkbox": "~3.1.0",
- "rc-collapse": "~3.7.0",
- "rc-dialog": "~9.1.0",
- "rc-drawer": "~6.2.0",
+ "rc-collapse": "~3.7.1",
+ "rc-dialog": "~9.3.4",
+ "rc-drawer": "~6.5.2",
"rc-dropdown": "~4.1.0",
- "rc-field-form": "~1.32.0",
- "rc-image": "~5.17.1",
- "rc-input": "~1.0.4",
- "rc-input-number": "~7.4.0",
- "rc-mentions": "~2.3.0",
- "rc-menu": "~9.9.2",
- "rc-motion": "^2.7.3",
- "rc-notification": "~5.0.4",
- "rc-pagination": "~3.5.0",
- "rc-picker": "~3.8.2",
- "rc-progress": "~3.4.1",
+ "rc-field-form": "~1.39.0",
+ "rc-image": "~7.3.1",
+ "rc-input": "~1.2.1",
+ "rc-input-number": "~8.1.0",
+ "rc-mentions": "~2.8.0",
+ "rc-menu": "~9.12.2",
+ "rc-motion": "^2.9.0",
+ "rc-notification": "~5.3.0",
+ "rc-pagination": "~3.6.1",
+ "rc-picker": "~3.14.6",
+ "rc-progress": "~3.5.1",
"rc-rate": "~2.12.0",
- "rc-resize-observer": "^1.2.0",
- "rc-segmented": "~2.2.0",
- "rc-select": "~14.5.0",
- "rc-slider": "~10.1.0",
- "rc-steps": "~6.0.0",
+ "rc-resize-observer": "^1.4.0",
+ "rc-segmented": "~2.2.2",
+ "rc-select": "~14.9.2",
+ "rc-slider": "~10.3.1",
+ "rc-steps": "~6.0.1",
"rc-switch": "~4.1.0",
- "rc-table": "~7.32.1",
- "rc-tabs": "~12.7.0",
- "rc-textarea": "~1.2.2",
- "rc-tooltip": "~6.0.0",
- "rc-tree": "~5.7.4",
- "rc-tree-select": "~5.9.0",
- "rc-upload": "~4.3.0",
- "rc-util": "^5.32.0",
+ "rc-table": "~7.34.4",
+ "rc-tabs": "~12.12.1",
+ "rc-textarea": "~1.4.0",
+ "rc-tooltip": "~6.1.2",
+ "rc-tree": "~5.7.12",
+ "rc-tree-select": "~5.13.0",
+ "rc-upload": "~4.3.5",
+ "rc-util": "^5.38.0",
"scroll-into-view-if-needed": "^3.0.3",
"throttle-debounce": "^5.0.0"
},
@@ -1349,14 +1902,14 @@
}
},
"node_modules/array-includes": {
- "version": "3.1.6",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
- "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
+ "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4",
- "get-intrinsic": "^1.1.3",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
"is-string": "^1.0.7"
},
"engines": {
@@ -1379,14 +1932,32 @@
"node": ">=8"
}
},
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz",
+ "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/array.prototype.flat": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
- "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
"es-shim-unscopables": "^1.0.0"
},
"engines": {
@@ -1397,13 +1968,13 @@
}
},
"node_modules/array.prototype.flatmap": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
- "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
"es-shim-unscopables": "^1.0.0"
},
"engines": {
@@ -1414,15 +1985,35 @@
}
},
"node_modules/array.prototype.tosorted": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz",
- "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz",
+ "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
"es-shim-unscopables": "^1.0.0",
- "get-intrinsic": "^1.1.3"
+ "get-intrinsic": "^1.2.1"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
+ "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-array-buffer": "^3.0.2",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asn1": {
@@ -1444,9 +2035,9 @@
}
},
"node_modules/ast-types-flow": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
- "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="
},
"node_modules/astral-regex": {
"version": "2.0.0",
@@ -1476,6 +2067,14 @@
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
},
+ "node_modules/asynciterator.prototype": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
+ "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1549,17 +2148,17 @@
"dev": true
},
"node_modules/axe-core": {
- "version": "4.7.2",
- "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
- "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/axios": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
- "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
+ "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -1617,14 +2216,6 @@
"tweetnacl": "^0.14.3"
}
},
- "node_modules/big-integer": {
- "version": "1.6.51",
- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
- "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
- "engines": {
- "node": ">=0.6"
- }
- },
"node_modules/blob-util": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
@@ -1637,17 +2228,6 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true
},
- "node_modules/bplist-parser": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
- "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
- "dependencies": {
- "big-integer": "^1.6.44"
- },
- "engines": {
- "node": ">= 5.10.0"
- }
- },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1668,10 +2248,20 @@
"node": ">=8"
}
},
+ "node_modules/browser-tabs-lock": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.3.0.tgz",
+ "integrity": "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==",
+ "hasInstallScript": true,
+ "peer": true,
+ "dependencies": {
+ "lodash": ">=4.17.21"
+ }
+ },
"node_modules/browserslist": {
- "version": "4.21.9",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
- "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
+ "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"funding": [
{
"type": "opencollective",
@@ -1687,10 +2277,10 @@
}
],
"dependencies": {
- "caniuse-lite": "^1.0.30001503",
- "electron-to-chromium": "^1.4.431",
- "node-releases": "^2.0.12",
- "update-browserslist-db": "^1.0.11"
+ "caniuse-lite": "^1.0.30001541",
+ "electron-to-chromium": "^1.4.535",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.13"
},
"bin": {
"browserslist": "cli.js"
@@ -1732,25 +2322,16 @@
"node": "*"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "peer": true
- },
- "node_modules/bundle-name": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
- "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
- "dependencies": {
- "run-applescript": "^5.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "peer": true
},
"node_modules/busboy": {
"version": "1.6.0",
@@ -1764,21 +2345,22 @@
}
},
"node_modules/cachedir": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
- "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
+ "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/call-bind": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
- "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"dependencies": {
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.0.2"
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1793,9 +2375,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001509",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz",
- "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==",
+ "version": "1.0.30001559",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz",
+ "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==",
"funding": [
{
"type": "opencollective",
@@ -1853,9 +2435,9 @@
}
},
"node_modules/character-entities": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
- "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -1907,9 +2489,9 @@
}
},
"node_modules/ci-info": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
- "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
@@ -2017,9 +2599,9 @@
}
},
"node_modules/comma-separated-tokens": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
- "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -2043,10 +2625,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
+ },
"node_modules/compute-scroll-into-view": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz",
- "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A=="
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz",
+ "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg=="
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -2079,6 +2666,22 @@
"node": ">= 0.6"
}
},
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
@@ -2093,6 +2696,26 @@
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-fetch": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
+ "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
+ "dependencies": {
+ "node-fetch": "^2.6.12"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2106,6 +2729,11 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
+ },
"node_modules/css-jss": {
"version": "10.10.0",
"resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz",
@@ -2131,15 +2759,15 @@
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/cypress": {
- "version": "12.16.0",
- "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.16.0.tgz",
- "integrity": "sha512-mwv1YNe48hm0LVaPgofEhGCtLwNIQEjmj2dJXnAkY1b4n/NE9OtgPph4TyS+tOtYp5CKtRmDvBzWseUXQTjbTg==",
+ "version": "13.4.0",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz",
+ "integrity": "sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
- "@cypress/request": "^2.88.10",
+ "@cypress/request": "^3.0.0",
"@cypress/xvfb": "^1.2.4",
- "@types/node": "^14.14.31",
+ "@types/node": "^18.17.5",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
"arch": "^2.2.0",
@@ -2172,9 +2800,10 @@
"minimist": "^1.2.8",
"ospath": "^1.2.2",
"pretty-bytes": "^5.6.0",
+ "process": "^0.11.10",
"proxy-from-env": "1.0.0",
"request-progress": "^3.0.0",
- "semver": "^7.3.2",
+ "semver": "^7.5.3",
"supports-color": "^8.1.1",
"tmp": "~0.2.1",
"untildify": "^4.0.0",
@@ -2184,14 +2813,17 @@
"cypress": "bin/cypress"
},
"engines": {
- "node": "^14.0.0 || ^16.0.0 || >=18.0.0"
+ "node": "^16.0.0 || ^18.0.0 || >=20.0.0"
}
},
"node_modules/cypress/node_modules/@types/node": {
- "version": "14.18.52",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.52.tgz",
- "integrity": "sha512-DGhiXKOHSFVVm+PJD+9Y0ObxXLeG6qwc0HoOn+ooQKeNNu+T2mEJCM5UBDUREKAggl9MHYjb5E71PAmx6MbzIg==",
- "dev": true
+ "version": "18.18.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.8.tgz",
+ "integrity": "sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/cypress/node_modules/proxy-from-env": {
"version": "1.0.0",
@@ -2217,9 +2849,9 @@
}
},
"node_modules/dayjs": {
- "version": "1.11.9",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
- "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+ "version": "1.11.10",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": {
"version": "4.3.4",
@@ -2249,181 +2881,30 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/decode-named-character-reference/node_modules/character-entities": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
},
- "node_modules/default-browser": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
- "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
- "dependencies": {
- "bundle-name": "^3.0.0",
- "default-browser-id": "^3.0.0",
- "execa": "^7.1.1",
- "titleize": "^3.0.0"
- },
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser-id": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
- "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
- "dependencies": {
- "bplist-parser": "^0.2.0",
- "untildify": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/execa": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz",
- "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^6.0.1",
- "human-signals": "^4.3.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^3.0.7",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
- "node_modules/default-browser/node_modules/get-stream": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/human-signals": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
- "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
- "engines": {
- "node": ">=14.18.0"
- }
- },
- "node_modules/default-browser/node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/npm-run-path": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
- "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser/node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "engines": {
- "node": ">=12"
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/define-lazy-prop": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
- "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">= 0.4"
}
},
"node_modules/define-properties": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
- "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dependencies": {
+ "define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
@@ -2489,10 +2970,16 @@
"node": ">=6.0.0"
}
},
- "node_modules/dom-align": {
- "version": "1.12.4",
- "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz",
- "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw=="
+ "node_modules/dotenv": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ }
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
@@ -2504,37 +2991,65 @@
"safer-buffer": "^2.1.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/editorconfig": {
- "version": "0.15.3",
- "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
- "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
"dependencies": {
- "commander": "^2.19.0",
- "lru-cache": "^4.1.5",
- "semver": "^5.6.0",
- "sigmund": "^1.0.1"
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
},
"bin": {
"editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
}
},
"node_modules/editorconfig/node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "engines": {
+ "node": ">=14"
+ }
},
- "node_modules/editorconfig/node_modules/semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "bin": {
- "semver": "bin/semver"
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/electron-to-chromium": {
- "version": "1.4.445",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.445.tgz",
- "integrity": "sha512-++DB+9VK8SBJwC+X1zlMfJ1tMA3F0ipi39GdEp+x3cV2TyBihqAgad8cNMWtLDEkbH39nlDQP7PfGrDr3Dr7HA=="
+ "version": "1.4.574",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.574.tgz",
+ "integrity": "sha512-bg1m8L0n02xRzx4LsTTMbBPiUd9yIR+74iPtS/Ao65CuXvhVZHP0ym1kSdDG3yHFDXqHQQBKujlN1AQ8qZnyFg=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -2563,36 +3078,38 @@
}
},
"node_modules/enquirer": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
- "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
"dependencies": {
- "ansi-colors": "^4.1.1"
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/es-abstract": {
- "version": "1.21.2",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
- "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+ "version": "1.22.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
+ "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
+ "arraybuffer.prototype.slice": "^1.0.2",
"available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.5",
"es-set-tostringtag": "^2.0.1",
"es-to-primitive": "^1.2.1",
- "function.prototype.name": "^1.1.5",
- "get-intrinsic": "^1.2.0",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.2",
"get-symbol-description": "^1.0.0",
"globalthis": "^1.0.3",
"gopd": "^1.0.1",
- "has": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
+ "hasown": "^2.0.0",
"internal-slot": "^1.0.5",
"is-array-buffer": "^3.0.2",
"is-callable": "^1.2.7",
@@ -2600,19 +3117,23 @@
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"is-string": "^1.0.7",
- "is-typed-array": "^1.1.10",
+ "is-typed-array": "^1.1.12",
"is-weakref": "^1.0.2",
- "object-inspect": "^1.12.3",
+ "object-inspect": "^1.13.1",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
- "regexp.prototype.flags": "^1.4.3",
+ "regexp.prototype.flags": "^1.5.1",
+ "safe-array-concat": "^1.0.1",
"safe-regex-test": "^1.0.0",
- "string.prototype.trim": "^1.2.7",
- "string.prototype.trimend": "^1.0.6",
- "string.prototype.trimstart": "^1.0.6",
+ "string.prototype.trim": "^1.2.8",
+ "string.prototype.trimend": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.7",
+ "typed-array-buffer": "^1.0.0",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
"typed-array-length": "^1.0.4",
"unbox-primitive": "^1.0.2",
- "which-typed-array": "^1.1.9"
+ "which-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
@@ -2621,31 +3142,52 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-iterator-helpers": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz",
+ "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==",
+ "dependencies": {
+ "asynciterator.prototype": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.1",
+ "es-set-tostringtag": "^2.0.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "globalthis": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "iterator.prototype": "^1.1.2",
+ "safe-array-concat": "^1.0.1"
+ }
+ },
"node_modules/es-module-lexer": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz",
- "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
+ "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
"peer": true
},
"node_modules/es-set-tostringtag": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
- "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
+ "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
"dependencies": {
- "get-intrinsic": "^1.1.3",
- "has": "^1.0.3",
- "has-tostringtag": "^1.0.0"
+ "get-intrinsic": "^1.2.2",
+ "has-tostringtag": "^1.0.0",
+ "hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-shim-unscopables": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
- "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
"dependencies": {
- "has": "^1.0.3"
+ "hasown": "^2.0.0"
}
},
"node_modules/es-to-primitive": {
@@ -2764,26 +3306,14 @@
}
}
},
- "node_modules/eslint-config-prettier": {
- "version": "8.8.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
- "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
- "dev": true,
- "bin": {
- "eslint-config-prettier": "bin/cli.js"
- },
- "peerDependencies": {
- "eslint": ">=7.0.0"
- }
- },
"node_modules/eslint-import-resolver-node": {
- "version": "0.3.7",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
- "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
"dependencies": {
"debug": "^3.2.7",
- "is-core-module": "^2.11.0",
- "resolve": "^1.22.1"
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
}
},
"node_modules/eslint-import-resolver-node/node_modules/debug": {
@@ -2795,18 +3325,17 @@
}
},
"node_modules/eslint-import-resolver-typescript": {
- "version": "3.5.5",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz",
- "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==",
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz",
+ "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==",
"dependencies": {
"debug": "^4.3.4",
"enhanced-resolve": "^5.12.0",
"eslint-module-utils": "^2.7.4",
+ "fast-glob": "^3.3.1",
"get-tsconfig": "^4.5.0",
- "globby": "^13.1.3",
"is-core-module": "^2.11.0",
- "is-glob": "^4.0.3",
- "synckit": "^0.8.5"
+ "is-glob": "^4.0.3"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -2819,35 +3348,6 @@
"eslint-plugin-import": "*"
}
},
- "node_modules/eslint-import-resolver-typescript/node_modules/globby": {
- "version": "13.2.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.0.tgz",
- "integrity": "sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==",
- "dependencies": {
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.11",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint-import-resolver-typescript/node_modules/slash": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
- "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/eslint-module-utils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
@@ -2873,25 +3373,27 @@
}
},
"node_modules/eslint-plugin-import": {
- "version": "2.27.5",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
- "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
- "dependencies": {
- "array-includes": "^3.1.6",
- "array.prototype.flat": "^1.3.1",
- "array.prototype.flatmap": "^1.3.1",
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
+ "integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlastindex": "^1.2.3",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
"debug": "^3.2.7",
"doctrine": "^2.1.0",
- "eslint-import-resolver-node": "^0.3.7",
- "eslint-module-utils": "^2.7.4",
- "has": "^1.0.3",
- "is-core-module": "^2.11.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.8.0",
+ "hasown": "^2.0.0",
+ "is-core-module": "^2.13.1",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
- "object.values": "^1.1.6",
- "resolve": "^1.22.1",
- "semver": "^6.3.0",
- "tsconfig-paths": "^3.14.1"
+ "object.fromentries": "^2.0.7",
+ "object.groupby": "^1.0.1",
+ "object.values": "^1.1.7",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.14.2"
},
"engines": {
"node": ">=4"
@@ -2920,34 +3422,34 @@
}
},
"node_modules/eslint-plugin-import/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-jsx-a11y": {
- "version": "6.7.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
- "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
- "dependencies": {
- "@babel/runtime": "^7.20.7",
- "aria-query": "^5.1.3",
- "array-includes": "^3.1.6",
- "array.prototype.flatmap": "^1.3.1",
- "ast-types-flow": "^0.0.7",
- "axe-core": "^4.6.2",
- "axobject-query": "^3.1.1",
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+ "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "aria-query": "^5.3.0",
+ "array-includes": "^3.1.7",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "=4.7.0",
+ "axobject-query": "^3.2.1",
"damerau-levenshtein": "^1.0.8",
"emoji-regex": "^9.2.2",
- "has": "^1.0.3",
- "jsx-ast-utils": "^3.3.3",
- "language-tags": "=1.0.5",
+ "es-iterator-helpers": "^1.0.15",
+ "hasown": "^2.0.0",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
"minimatch": "^3.1.2",
- "object.entries": "^1.1.6",
- "object.fromentries": "^2.0.6",
- "semver": "^6.3.0"
+ "object.entries": "^1.1.7",
+ "object.fromentries": "^2.0.7"
},
"engines": {
"node": ">=4.0"
@@ -2956,23 +3458,16 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
}
},
- "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-react": {
- "version": "7.32.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
- "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==",
+ "version": "7.33.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
+ "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
"array.prototype.tosorted": "^1.1.1",
"doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.0.12",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
@@ -2982,7 +3477,7 @@
"object.values": "^1.1.6",
"prop-types": "^15.8.1",
"resolve": "^2.0.0-next.4",
- "semver": "^6.3.0",
+ "semver": "^6.3.1",
"string.prototype.matchall": "^4.0.8"
},
"engines": {
@@ -3015,11 +3510,11 @@
}
},
"node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.4",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
- "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
"dependencies": {
- "is-core-module": "^2.9.0",
+ "is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -3031,17 +3526,17 @@
}
},
"node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/eslint-scope": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
- "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -3054,9 +3549,9 @@
}
},
"node_modules/eslint-visitor-keys": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
- "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -3065,11 +3560,11 @@
}
},
"node_modules/espree": {
- "version": "9.5.2",
- "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
- "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dependencies": {
- "acorn": "^8.8.0",
+ "acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
@@ -3278,9 +3773,9 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
- "version": "3.2.12",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
- "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -3342,6 +3837,11 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fflate": {
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
+ "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
+ },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -3404,26 +3904,27 @@
}
},
"node_modules/flat-cache": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
- "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz",
+ "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==",
"dependencies": {
- "flatted": "^3.1.0",
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": ">=12.0.0"
}
},
"node_modules/flatted": {
- "version": "3.2.7",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
- "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
"node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
@@ -3478,15 +3979,15 @@
}
},
"node_modules/fraction.js": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
- "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
- "url": "https://www.patreon.com/infusion"
+ "url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fresh": {
@@ -3518,20 +4019,36 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/function.prototype.name": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
- "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.0",
- "functions-have-names": "^1.2.2"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
},
"engines": {
"node": ">= 0.4"
@@ -3549,14 +4066,14 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
- "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"dependencies": {
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
+ "function-bind": "^1.1.2",
"has-proto": "^1.0.1",
- "has-symbols": "^1.0.3"
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3593,9 +4110,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz",
- "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==",
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
+ "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
@@ -3673,9 +4190,9 @@
}
},
"node_modules/globals": {
- "version": "13.20.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
- "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "version": "13.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -3740,17 +4257,6 @@
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
},
- "node_modules/has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dependencies": {
- "function-bind": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -3768,11 +4274,11 @@
}
},
"node_modules/has-property-descriptors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
- "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"dependencies": {
- "get-intrinsic": "^1.1.1"
+ "get-intrinsic": "^1.2.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3814,6 +4320,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -3849,33 +4366,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/hast-util-to-estree/node_modules/comma-separated-tokens": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
- "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/hast-util-to-estree/node_modules/property-information": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
- "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/hast-util-to-estree/node_modules/space-separated-tokens": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/hast-util-whitespace": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz",
@@ -3901,6 +4391,41 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hastscript/node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hastscript/node_modules/property-information": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
+ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hastscript/node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/highlight-words-core": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.2.tgz",
+ "integrity": "sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg=="
+ },
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
@@ -3931,6 +4456,18 @@
"node": ">=0.10"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "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",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/human-signals": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
@@ -3973,6 +4510,11 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -4005,6 +4547,14 @@
"node": ">=8"
}
},
+ "node_modules/inflation": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz",
+ "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4034,18 +4584,23 @@
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
"node_modules/internal-slot": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
- "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
+ "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
"dependencies": {
- "get-intrinsic": "^1.2.0",
- "has": "^1.0.3",
+ "get-intrinsic": "^1.2.2",
+ "hasown": "^2.0.0",
"side-channel": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
}
},
+ "node_modules/intl-tel-input": {
+ "version": "17.0.21",
+ "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-17.0.21.tgz",
+ "integrity": "sha512-TfyPxLe41QZPOf6RqBxRE2dpQ0FThB/PBD/gRbxVhGW7IuYg30QD90x/vjmEo4vkZw7j8etxpVcjIZVRcG+Otw=="
+ },
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
@@ -4081,6 +4636,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
@@ -4153,11 +4722,11 @@
}
},
"node_modules/is-core-module": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
- "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dependencies": {
- "has": "^1.0.3"
+ "hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4186,20 +4755,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-docker": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
- "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4208,6 +4763,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-finalizationregistry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+ "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -4217,6 +4783,20 @@
"node": ">=8"
}
},
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -4242,23 +4822,6 @@
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g=="
},
- "node_modules/is-inside-container": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
- "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
- "dependencies": {
- "is-docker": "^3.0.0"
- },
- "bin": {
- "is-inside-container": "cli.js"
- },
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-installed-globally": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@@ -4275,6 +4838,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
+ "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -4328,9 +4899,9 @@
}
},
"node_modules/is-reference": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz",
- "integrity": "sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
+ "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dependencies": {
"@types/estree": "*"
}
@@ -4350,6 +4921,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-set": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
+ "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@@ -4365,6 +4944,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -4401,15 +4981,11 @@
}
},
"node_modules/is-typed-array": {
- "version": "1.1.10",
- "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
- "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+ "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"dependencies": {
- "available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
- "for-each": "^0.3.3",
- "gopd": "^1.0.1",
- "has-tostringtag": "^1.0.0"
+ "which-typed-array": "^1.1.11"
},
"engines": {
"node": ">= 0.4"
@@ -4436,6 +5012,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-weakmap": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
+ "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -4447,31 +5031,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-wsl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
- "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "node_modules/is-weakset": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
+ "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
"dependencies": {
- "is-docker": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-wsl/node_modules/is-docker": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
- "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": ">=8"
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4483,6 +5059,18 @@
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"dev": true
},
+ "node_modules/iterator.prototype": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+ "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "reflect.getprototypeof": "^1.0.4",
+ "set-function-name": "^2.0.1"
+ }
+ },
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@@ -4497,13 +5085,41 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/jose": {
+ "version": "4.15.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
+ "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/jotai": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.5.0.tgz",
+ "integrity": "sha512-iPJDFrhNw3KU4o4SSAESeZoW+nM1L/76VULXZWF23qmivBEcbAQjiF5n1W4Hn5dXAol4tmtEYe4HH7d9emEz0Q==",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=17.0.0",
+ "react": ">=17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/js-beautify": {
- "version": "1.14.8",
- "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.8.tgz",
- "integrity": "sha512-4S7HFeI9YfRvRgKnEweohs0tgJj28InHVIj4Nl8Htf96Y6pHg3+tJrmo4ucAM9f7l4SHbFI3IvFAZ2a1eQPbyg==",
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz",
+ "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==",
"dependencies": {
"config-chain": "^1.1.13",
- "editorconfig": "^0.15.3",
+ "editorconfig": "^1.0.3",
"glob": "^8.1.0",
"nopt": "^6.0.0"
},
@@ -4554,9 +5170,9 @@
}
},
"node_modules/js-sdsl": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.1.tgz",
- "integrity": "sha512-6Gsx8R0RucyePbWqPssR8DyfuXmLBooYN5cZFZKjHGnQuaf7pEzhtpceagJxVu4LqhYY5EYA7nko3FmeHZ1KbA==",
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz",
+ "integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
@@ -4584,6 +5200,11 @@
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true
},
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+ },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -4643,6 +5264,27 @@
"graceful-fs": "^4.1.6"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
"node_modules/jsprim": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -4811,9 +5453,9 @@
}
},
"node_modules/jsx-ast-utils": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.4.tgz",
- "integrity": "sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==",
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flat": "^1.3.1",
@@ -4824,6 +5466,33 @@
"node": ">=4.0"
}
},
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -4838,11 +5507,14 @@
"integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w=="
},
"node_modules/language-tags": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
- "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
"dependencies": {
- "language-subtag-registry": "~0.3.2"
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
}
},
"node_modules/lazy-ass": {
@@ -4866,6 +5538,19 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.10.49",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz",
+ "integrity": "sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ=="
+ },
+ "node_modules/lie": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+ "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/listr2": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
@@ -4907,6 +5592,14 @@
"node": ">=6.11.5"
}
},
+ "node_modules/localforage": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
+ "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
+ "dependencies": {
+ "lie": "3.1.1"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4924,8 +5617,37 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -4935,8 +5657,7 @@
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
- "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
- "dev": true
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/log-symbols": {
"version": "4.1.0",
@@ -5037,12 +5758,25 @@
}
},
"node_modules/lru-cache": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
- "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
- "pseudomap": "^1.0.2",
- "yallist": "^2.1.2"
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
+ "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/markdown-extensions": {
@@ -5145,15 +5879,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdast-util-mdx-jsx/node_modules/character-entities": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
- "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/mdast-util-mdx-jsx/node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
@@ -5319,6 +6044,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/memoize-one": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz",
+ "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw=="
+ },
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -5962,6 +6692,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -5985,6 +6716,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/monaco-editor": {
+ "version": "0.44.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz",
+ "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==",
+ "peer": true
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -6110,10 +6858,40 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/nextjs-cors": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/nextjs-cors/-/nextjs-cors-2.1.2.tgz",
+ "integrity": "sha512-2yOVivaaf2ILe4f/qY32hnj3oC77VCOsUQJQfhVMGsXE/YMEWUY2zy78sH9FKUCM7eG42/l3pDofIzMD781XGA==",
+ "dependencies": {
+ "cors": "^2.8.5"
+ },
+ "peerDependencies": {
+ "next": "^8.1.1-canary.54 || ^9.0.0 || ^10.0.0-0 || ^11.0.0 || ^12.0.0 || ^13.0.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "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"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-mocks-http": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.12.2.tgz",
- "integrity": "sha512-xhWwC0dh35R9rf0j3bRZXuISXdHxxtMx0ywZQBwjrg3yl7KpRETzogfeCamUIjltpn0Fxvs/ZhGJul1vPLrdJQ==",
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.13.0.tgz",
+ "integrity": "sha512-lArD6sJMPJ53WF50GX0nJ89B1nkV1TdMvNwq8WXXFrUXF80ujSyye1T30mgiHh4h2It0/svpF3C4kZ2OAONVlg==",
"dev": true,
"dependencies": {
"accepts": "^1.3.7",
@@ -6128,13 +6906,21 @@
"type-is": "^1.6.18"
},
"engines": {
- "node": ">=0.6"
+ "node": ">=14"
}
},
"node_modules/node-releases": {
- "version": "2.0.12",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
- "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ=="
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ },
+ "node_modules/nodemailer": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz",
+ "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
},
"node_modules/nopt": {
"version": "6.0.0",
@@ -6162,6 +6948,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
"dependencies": {
"path-key": "^3.0.0"
},
@@ -6178,9 +6965,9 @@
}
},
"node_modules/object-inspect": {
- "version": "1.12.3",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
- "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -6211,26 +6998,26 @@
}
},
"node_modules/object.entries": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz",
- "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==",
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz",
+ "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/object.fromentries": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz",
- "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==",
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
+ "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"engines": {
"node": ">= 0.4"
@@ -6239,26 +7026,37 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object.groupby": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz",
+ "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1"
+ }
+ },
"node_modules/object.hasown": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz",
- "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==",
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz",
+ "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==",
"dependencies": {
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object.values": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
- "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
+ "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"engines": {
"node": ">= 0.4"
@@ -6279,6 +7077,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
"dependencies": {
"mimic-fn": "^2.1.0"
},
@@ -6289,23 +7088,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/open": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
- "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
- "dependencies": {
- "default-browser": "^4.0.0",
- "define-lazy-prop": "^3.0.0",
- "is-inside-container": "^1.0.0",
- "is-wsl": "^2.2.0"
- },
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -6399,6 +7181,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/parse-entities/node_modules/character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -6492,6 +7283,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-3.1.0.tgz",
+ "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==",
+ "dependencies": {
+ "crypto-js": "^4.1.1"
+ }
+ },
"node_modules/postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
@@ -6524,6 +7323,14 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
+ "node_modules/posthog-js": {
+ "version": "1.87.6",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.87.6.tgz",
+ "integrity": "sha512-ocVNosPVXViFfdRi1VB5IVGLgTvFerIljJ308v9irQ3MZosuegs6tPFFCOefr6RUmyQF2gI/mDbMZafRoFS+KA==",
+ "dependencies": {
+ "fflate": "^0.4.1"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6533,9 +7340,9 @@
}
},
"node_modules/prettier": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
- "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
+ "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -6567,6 +7374,23 @@
"node": ">=6"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6578,12 +7402,9 @@
}
},
"node_modules/property-information": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
- "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
- "dependencies": {
- "xtend": "^4.0.0"
- },
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.0.tgz",
+ "integrity": "sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -6599,16 +7420,10 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
- "node_modules/pseudomap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
- "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ=="
- },
"node_modules/psl": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
- "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
- "dev": true
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
},
"node_modules/pump": {
"version": "3.0.0",
@@ -6621,9 +7436,9 @@
}
},
"node_modules/punycode": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
- "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
@@ -6640,7 +7455,6 @@
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
- "dev": true,
"dependencies": {
"side-channel": "^1.0.4"
},
@@ -6651,6 +7465,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6688,33 +7507,17 @@
"node": ">= 0.6"
}
},
- "node_modules/rc-align": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz",
- "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==",
- "dependencies": {
- "@babel/runtime": "^7.10.1",
- "classnames": "2.x",
- "dom-align": "^1.7.0",
- "rc-util": "^5.26.0",
- "resize-observer-polyfill": "^1.5.1"
- },
- "peerDependencies": {
- "react": ">=16.9.0",
- "react-dom": ">=16.9.0"
- }
- },
"node_modules/rc-cascader": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.12.0.tgz",
- "integrity": "sha512-QTeGPTNYX33alozNy9lYg7YKpvYVwquai/mrFRR8mHlHnK7QlqJyMqbs2p7rc5eeKARKMRTUeoN5CfO+Gr9UBw==",
+ "version": "3.18.1",
+ "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.18.1.tgz",
+ "integrity": "sha512-M7Xr5Fs/E87ZGustfObtBYQjsvBCET0UX2JYXB2GmOP+2fsZgjaRGXK+CJBmmWXQ6o4OFinpBQBXG4wJOQ5MEg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"array-tree-filter": "^2.1.0",
"classnames": "^2.3.1",
- "rc-select": "~14.5.0",
+ "rc-select": "~14.9.0",
"rc-tree": "~5.7.0",
- "rc-util": "^5.6.1"
+ "rc-util": "^5.35.0"
},
"peerDependencies": {
"react": ">=16.9.0",
@@ -6736,9 +7539,9 @@
}
},
"node_modules/rc-collapse": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.0.tgz",
- "integrity": "sha512-Cir1c89cENiK5wryd9ut+XltrIfx/+KH1/63uJIVjuXkgfrIvIy6W1fYGgEYtttbHW2fEfxg1s31W+Vm98fSRw==",
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.1.tgz",
+ "integrity": "sha512-N/7ejyiTf3XElNJBBpxqnZBUuMsQWEOPjB2QkfNvZ/Ca54eAvJXuOD1EGbCWCk2m7v/MSxku7mRpdeaLOCd4Gg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
@@ -6751,9 +7554,9 @@
}
},
"node_modules/rc-dialog": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.1.0.tgz",
- "integrity": "sha512-5ry+JABAWEbaKyYsmITtrJbZbJys8CtMyzV8Xn4LYuXMeUx5XVHNyJRoqLFE4AzBuXXzOWeaC49cg+XkxK6kHA==",
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz",
+ "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/portal": "^1.0.0-8",
@@ -6767,15 +7570,15 @@
}
},
"node_modules/rc-drawer": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-6.2.0.tgz",
- "integrity": "sha512-spPkZ3WvP0U0vy5dyzSwlUJ/+vLFtjP/cTwSwejhQRoDBaexSZHsBhELoCZcEggI7LQ7typmtG30lAue2HEhvA==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-6.5.2.tgz",
+ "integrity": "sha512-QckxAnQNdhh4vtmKN0ZwDf3iakO83W9eZcSKWYYTDv4qcD2fHhRAZJJ/OE6v2ZlQ2kSqCJX5gYssF4HJFvsEPQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/portal": "^1.1.1",
"classnames": "^2.2.6",
"rc-motion": "^2.6.1",
- "rc-util": "^5.21.2"
+ "rc-util": "^5.36.0"
},
"peerDependencies": {
"react": ">=16.9.0",
@@ -6798,9 +7601,9 @@
}
},
"node_modules/rc-field-form": {
- "version": "1.32.2",
- "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.32.2.tgz",
- "integrity": "sha512-SzqG1YGyD2P42ztZJ7qoPQp6FV9bD51RUdKGG/5xwybU1wbFdgWTqiMXkS8UR9L4GwXVMKh5PaF2I4EBXd/Rng==",
+ "version": "1.39.0",
+ "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.39.0.tgz",
+ "integrity": "sha512-V7Wk7uji1jBsUGGgP788H9rpFy55HLiD4lywTlktUGjK7EgW5dt+mq1MPbtCpPRMzs83vZBW4SOChOmCACz4WA==",
"dependencies": {
"@babel/runtime": "^7.18.0",
"async-validator": "^4.1.0",
@@ -6815,16 +7618,16 @@
}
},
"node_modules/rc-image": {
- "version": "5.17.1",
- "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-5.17.1.tgz",
- "integrity": "sha512-oR4eviLyQxd/5A7pn843w2/Z1wuBA27L2lS4agq0sjl2z97ssNIVEzRzgwgB0ZxVZG/qSu9Glit2Zgzb/n+blQ==",
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.3.2.tgz",
+ "integrity": "sha512-ICEF6SWv9YKhDXxy1vrXcmf0TVvEcQWIww5Yg+f+mn7e4oGX7FNP4+FExwMjNO5UHBEuWrigbGhlCgI6yZZ1jg==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@rc-component/portal": "^1.0.2",
"classnames": "^2.2.6",
- "rc-dialog": "~9.1.0",
+ "rc-dialog": "~9.3.4",
"rc-motion": "^2.6.2",
- "rc-util": "^5.0.6"
+ "rc-util": "^5.34.1"
},
"peerDependencies": {
"react": ">=16.9.0",
@@ -6832,9 +7635,9 @@
}
},
"node_modules/rc-input": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.0.4.tgz",
- "integrity": "sha512-clY4oneVHRtKHYf/HCxT/MO+4BGzCIywSNLosXWOm7fcQAS0jQW7n0an8Raa8JMB8kpxc8m28p7SNwFZmlMj6g==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.2.1.tgz",
+ "integrity": "sha512-nQRmBvEFoGi+SNRDavccZ8ueyhFgmxkWqIt4aDyuNJgUZF12HJKIwDhAafUM7N+g7PyuW9FH3pf3zPHzdiCWbA==",
"dependencies": {
"@babel/runtime": "^7.11.1",
"classnames": "^2.2.1",
@@ -6846,13 +7649,14 @@
}
},
"node_modules/rc-input-number": {
- "version": "7.4.2",
- "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-7.4.2.tgz",
- "integrity": "sha512-yGturTw7WGP+M1GbJ+UTAO7L4buxeW6oilhL9Sq3DezsRS8/9qec4UiXUbeoiX9bzvRXH11JvgskBtxSp4YSNg==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-8.1.0.tgz",
+ "integrity": "sha512-bdHgduOxuN0lrhzgPmoKbhRD4GLIzVcddVz972/JHPHr7oLwPX5xDb9w4bXhuMzyT2VzQy7nggRCfH3yAl09oA==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/mini-decimal": "^1.0.1",
"classnames": "^2.2.5",
+ "rc-input": "~1.2.1",
"rc-util": "^5.28.0"
},
"peerDependencies": {
@@ -6861,17 +7665,17 @@
}
},
"node_modules/rc-mentions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.3.0.tgz",
- "integrity": "sha512-gNpsSKsBHSXvyAA1ZowVTqXSWUIw7+OI9wmjL87KcYURvtm9nDo8R0KtOc2f1PT7q9McUpFzhm6AvQdIly0aRA==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.8.0.tgz",
+ "integrity": "sha512-LBMkO6bSGhEvS1CvMK978qGN82tI+mzk7l/uTiQJH+UDiwpvq+pxK4DxU5b6Q1T5LW6bn2pSua9RaZKZrDoBOw==",
"dependencies": {
- "@babel/runtime": "^7.10.1",
+ "@babel/runtime": "^7.22.5",
"@rc-component/trigger": "^1.5.0",
"classnames": "^2.2.6",
- "rc-input": "~1.0.0",
- "rc-menu": "~9.9.0",
- "rc-textarea": "~1.2.0",
- "rc-util": "^5.22.5"
+ "rc-input": "~1.2.1",
+ "rc-menu": "~9.12.0",
+ "rc-textarea": "~1.4.0",
+ "rc-util": "^5.34.1"
},
"peerDependencies": {
"react": ">=16.9.0",
@@ -6879,15 +7683,15 @@
}
},
"node_modules/rc-menu": {
- "version": "9.9.2",
- "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.9.2.tgz",
- "integrity": "sha512-kVJwaQn5VUu6DIddxd/jz3QupTPg0tNYq+mpFP8wYsRF5JgzPA9fPVw+CfwlTPwA1w7gzEY42S8pj6M3uev5CQ==",
+ "version": "9.12.2",
+ "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.12.2.tgz",
+ "integrity": "sha512-NzloFH2pRUYmQ3S/YbJAvRkgCZaLvq0sRa5rgJtuIHLfPPprNHNyepeSlT64+dbVqI4qRWL44VN0lUCldCbbfg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
- "@rc-component/trigger": "^1.6.2",
+ "@rc-component/trigger": "^1.17.0",
"classnames": "2.x",
"rc-motion": "^2.4.3",
- "rc-overflow": "^1.2.8",
+ "rc-overflow": "^1.3.1",
"rc-util": "^5.27.0"
},
"peerDependencies": {
@@ -6896,9 +7700,9 @@
}
},
"node_modules/rc-motion": {
- "version": "2.7.3",
- "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.7.3.tgz",
- "integrity": "sha512-2xUvo8yGHdOHeQbdI8BtBsCIrWKchEmFEIskf0nmHtJsou+meLd/JE+vnvSX2JxcBrJtXY2LuBpxAOxrbY/wMQ==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.0.tgz",
+ "integrity": "sha512-XIU2+xLkdIr1/h6ohPZXyPBMvOmuyFZQ/T0xnawz+Rh+gh4FINcnZmMT5UTIj6hgI0VLDjTaPeRd+smJeSPqiQ==",
"dependencies": {
"@babel/runtime": "^7.11.1",
"classnames": "^2.2.1",
@@ -6910,13 +7714,13 @@
}
},
"node_modules/rc-notification": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.0.4.tgz",
- "integrity": "sha512-3535oellIRlt1LspERfK8yvCqb8Gio3R02rULciaSc1xe3H7ArTU/khlUTv1ddGzua4HhmF4D4Rwz/+mBxETvg==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz",
+ "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
- "rc-motion": "^2.6.0",
+ "rc-motion": "^2.9.0",
"rc-util": "^5.20.1"
},
"engines": {
@@ -6928,14 +7732,14 @@
}
},
"node_modules/rc-overflow": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.1.tgz",
- "integrity": "sha512-RY0nVBlfP9CkxrpgaLlGzkSoh9JhjJLu6Icqs9E7CW6Ewh9s0peF9OHIex4OhfoPsR92LR0fN6BlCY9Z4VoUtA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz",
+ "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==",
"dependencies": {
"@babel/runtime": "^7.11.1",
"classnames": "^2.2.1",
"rc-resize-observer": "^1.0.0",
- "rc-util": "^5.19.2"
+ "rc-util": "^5.37.0"
},
"peerDependencies": {
"react": ">=16.9.0",
@@ -6943,9 +7747,9 @@
}
},
"node_modules/rc-pagination": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-3.5.0.tgz",
- "integrity": "sha512-lUBVtVVUn7gGsq4mTyVpcZQr+AMcljbMiL/HcCmSdFrcsK0iZVKwwbXDxhz2IV0JXUs9Hzepr5sQFaF+9ad/pQ==",
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-3.6.1.tgz",
+ "integrity": "sha512-R/sUnKKXx1Nm4kZfUKS3YKa7yEPF1ZkVB/AynQaHt+nMER7h9wPTfliDJFdYo+RM/nk2JD4Yc5QpUq8fIQHeug==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.1",
@@ -6957,9 +7761,9 @@
}
},
"node_modules/rc-picker": {
- "version": "3.8.2",
- "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-3.8.2.tgz",
- "integrity": "sha512-q6jnMwBoOi6tFA4xohrKIhzq80Fc3dH0Kiw5VRx6Tf1db7y27PBFCLwu6f66niXidZKD8F4R0M9VIui/jkL4cg==",
+ "version": "3.14.6",
+ "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-3.14.6.tgz",
+ "integrity": "sha512-AdKKW0AqMwZsKvIpwUWDUnpuGKZVrbxVTZTNjcO+pViGkjC1EBcjMgxVe8tomOEaIHJL5Gd13vS8Rr3zzxWmag==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/trigger": "^1.5.0",
@@ -6993,9 +7797,9 @@
}
},
"node_modules/rc-progress": {
- "version": "3.4.2",
- "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.4.2.tgz",
- "integrity": "sha512-iAGhwWU+tsayP+Jkl9T4+6rHeQTG9kDz8JAHZk4XtQOcYN5fj9H34NXNEdRdZx94VUDHMqCb1yOIvi8eJRh67w==",
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz",
+ "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.6",
@@ -7024,13 +7828,13 @@
}
},
"node_modules/rc-resize-observer": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.3.1.tgz",
- "integrity": "sha512-iFUdt3NNhflbY3mwySv5CA1TC06zdJ+pfo0oc27xpf4PIOvfZwZGtD9Kz41wGYqC4SLio93RVAirSSpYlV/uYg==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz",
+ "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==",
"dependencies": {
"@babel/runtime": "^7.20.7",
"classnames": "^2.2.1",
- "rc-util": "^5.27.0",
+ "rc-util": "^5.38.0",
"resize-observer-polyfill": "^1.5.1"
},
"peerDependencies": {
@@ -7054,15 +7858,15 @@
}
},
"node_modules/rc-select": {
- "version": "14.5.2",
- "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.5.2.tgz",
- "integrity": "sha512-Np/lDHvxCnVhVsheQjSV1I/OMJTWJf1n10wq8q1AGy3ytyYLfjNpi6uaz/pmjsbbiSddSWzJnNZCli9LmgBZsA==",
+ "version": "14.9.2",
+ "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.9.2.tgz",
+ "integrity": "sha512-VQ15sRFgPURHb8ZcZNSDtb2rAw3+C9xlL0nDziwNHTEW1KvEpZ8y+0v5w24X/Bpl9b3cW1BOyW1F5UqSAq+7Dg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/trigger": "^1.5.0",
"classnames": "2.x",
"rc-motion": "^2.0.1",
- "rc-overflow": "^1.0.0",
+ "rc-overflow": "^1.3.1",
"rc-util": "^5.16.1",
"rc-virtual-list": "^3.5.2"
},
@@ -7075,9 +7879,9 @@
}
},
"node_modules/rc-slider": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.1.1.tgz",
- "integrity": "sha512-gn8oXazZISEhnmRinI89Z/JD/joAaM35jp+gDtIVSTD/JJMCCBqThqLk1SVJmvtfeiEF/kKaFY0+qt4SDHFUDw==",
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.3.1.tgz",
+ "integrity": "sha512-XszsZLkbjcG9ogQy/zUC0n2kndoKUAnY/Vnk1Go5Gx+JJQBz0Tl15d5IfSiglwBUZPS9vsUJZkfCmkIZSqWbcA==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.5",
@@ -7092,9 +7896,9 @@
}
},
"node_modules/rc-steps": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.0.tgz",
- "integrity": "sha512-+KfMZIty40mYCQSDvYbZ1jwnuObLauTiIskT1hL4FFOBHP6ZOr8LK0m143yD3kEN5XKHSEX1DIwCj3AYZpoeNQ==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
+ "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
"dependencies": {
"@babel/runtime": "^7.16.7",
"classnames": "^2.2.3",
@@ -7123,15 +7927,16 @@
}
},
"node_modules/rc-table": {
- "version": "7.32.1",
- "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.32.1.tgz",
- "integrity": "sha512-fHMQteKMocUC9I9Vex3eBLH7QsiaMR/qtzh3B1Ty2PoNGwVTwVdDFyRL05zch+JU3KnNNczgQeVvtf/p//gdrQ==",
+ "version": "7.34.4",
+ "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.34.4.tgz",
+ "integrity": "sha512-os+i88Y2AO/6dNkOgJkKSHgXYaZZGnuOEEe+nyaq5IRgvAQNhLysUjXt2objtBeFDEZR8TqXrajwBNRUwunmdw==",
"dependencies": {
"@babel/runtime": "^7.10.1",
- "@rc-component/context": "^1.3.0",
+ "@rc-component/context": "^1.4.0",
"classnames": "^2.2.5",
"rc-resize-observer": "^1.1.0",
- "rc-util": "^5.27.1"
+ "rc-util": "^5.36.0",
+ "rc-virtual-list": "^3.11.1"
},
"engines": {
"node": ">=8.x"
@@ -7142,17 +7947,17 @@
}
},
"node_modules/rc-tabs": {
- "version": "12.7.1",
- "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-12.7.1.tgz",
- "integrity": "sha512-NrltXEYIyiDP5JFu85NQwc9eR+7e50r/6MNXYDyG1EMIFNc7BgDppzdpnD3nW4NHYWw5wLIThCURGib48OCTBg==",
+ "version": "12.12.1",
+ "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-12.12.1.tgz",
+ "integrity": "sha512-e10VBjEkECdPl4XZSs9to81SE+mgclBTM7J8/LMsFqmJoi05Tci91bRnmeeDtrcOCx2PuZdJv57XUlC4d8PEIw==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"classnames": "2.x",
"rc-dropdown": "~4.1.0",
- "rc-menu": "~9.9.0",
+ "rc-menu": "~9.12.0",
"rc-motion": "^2.6.2",
"rc-resize-observer": "^1.0.0",
- "rc-util": "^5.16.0"
+ "rc-util": "^5.34.1"
},
"engines": {
"node": ">=8.x"
@@ -7163,13 +7968,13 @@
}
},
"node_modules/rc-textarea": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.2.3.tgz",
- "integrity": "sha512-YvN8IskIVBRRzcS4deT0VAMim31+T3IoVX4yoCJ+b/iVCvw7yf0usR7x8OaHiUOUoURKcn/3lfGjmtzplcy99g==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.4.0.tgz",
+ "integrity": "sha512-CiqK+uyoJlnfufbC0kwfHJpfElhQacuDSNyNQ/xGnA/QMaJLDbgmqRT8QmX0T0KD/ws/hy6qqRaGJSsrRR5uiQ==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "^2.2.1",
- "rc-input": "~1.0.4",
+ "rc-input": "~1.2.1",
"rc-resize-observer": "^1.0.0",
"rc-util": "^5.27.0"
},
@@ -7179,12 +7984,12 @@
}
},
"node_modules/rc-tooltip": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.0.1.tgz",
- "integrity": "sha512-MdvPlsD1fDSxKp9+HjXrc/CxLmA/s11QYIh1R7aExxfodKP7CZA++DG1AjrW80F8IUdHYcR43HAm0Y2BYPelHA==",
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.1.2.tgz",
+ "integrity": "sha512-89zwvybvCxGJu3+gGF8w5AXd4HHk6hIN7K0vZbkzjilVaEAIWPqc1fcyeUeP71n3VCcw7pTL9LyFupFbrx8gHw==",
"dependencies": {
"@babel/runtime": "^7.11.2",
- "@rc-component/trigger": "^1.0.4",
+ "@rc-component/trigger": "^1.18.0",
"classnames": "^2.3.1"
},
"peerDependencies": {
@@ -7193,9 +7998,9 @@
}
},
"node_modules/rc-tree": {
- "version": "5.7.6",
- "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.7.6.tgz",
- "integrity": "sha512-Dzam4VFcohXfcw+K4syq177RKqdqYun1XRc6etAEpRvsTruo4udhcsPrsEfOrRkrhnmkO58Q9F1/lgvm2dznVQ==",
+ "version": "5.7.12",
+ "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.7.12.tgz",
+ "integrity": "sha512-LXA5nY2hG5koIAlHW5sgXgLpOMz+bFRbnZZ+cCg0tQs4Wv1AmY7EDi1SK7iFXhslYockbqUerQan82jljoaItg==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
@@ -7212,13 +8017,13 @@
}
},
"node_modules/rc-tree-select": {
- "version": "5.9.0",
- "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.9.0.tgz",
- "integrity": "sha512-oh3blESzLfLCBPSiVDtZ2irzrWWZUMeHvnSwRvFo79br8Z+K/1OhXhXBZmROvfKwaH8YUugAQy8B2j5EGQbdyA==",
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.13.0.tgz",
+ "integrity": "sha512-g01JU9EdE7j/9KfDKtmvFqJ7ZDNIYDzkpmAXllbTBFoRNhWJBjW1x/dCZLVG+IdZeIz8SKJkgZzCf1CUZrzV/Q==",
"dependencies": {
"@babel/runtime": "^7.10.1",
"classnames": "2.x",
- "rc-select": "~14.5.0",
+ "rc-select": "~14.9.0",
"rc-tree": "~5.7.0",
"rc-util": "^5.16.1"
},
@@ -7228,9 +8033,9 @@
}
},
"node_modules/rc-upload": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.3.4.tgz",
- "integrity": "sha512-uVbtHFGNjHG/RyAfm9fluXB6pvArAGyAx8z7XzXXyorEgVIWj6mOlriuDm0XowDHYz4ycNK0nE0oP3cbFnzxiQ==",
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.3.5.tgz",
+ "integrity": "sha512-EHlKJbhkgFSQHliTj9v/2K5aEuFwfUQgZARzD7AmAPOneZEPiCNF3n6PEWIuqz9h7oq6FuXgdR67sC5BWFxJbA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"classnames": "^2.2.5",
@@ -7242,27 +8047,32 @@
}
},
"node_modules/rc-util": {
- "version": "5.34.0",
- "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.34.0.tgz",
- "integrity": "sha512-+zCDJ1gq+KwqbaZPAk7PGlNAssoTcnZSnTsr5KMYDBhzdPNFxyuglnewWMP5PyP/kAC6uW4r9Ejc08M+Lei04A==",
+ "version": "5.38.0",
+ "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.0.tgz",
+ "integrity": "sha512-yV/YBNdFn+edyBpBdCqkPE29Su0jWcHNgwx2dJbRqMrMfrUcMJUjCRV+ZPhcvWyKFJ63GzEerPrz9JIVo0zXmA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
- "react-is": "^16.12.0"
+ "react-is": "^18.2.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
+ "node_modules/rc-util/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
"node_modules/rc-virtual-list": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz",
- "integrity": "sha512-sE2G9hTPjVmatQni8OP2Kx33+Oth6DMKm67OblBBmgMBJDJQOOFpSGH7KZ6Pm85rrI2IGxDRXZCr0QhYOH2pfQ==",
+ "version": "3.11.3",
+ "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.3.tgz",
+ "integrity": "sha512-tu5UtrMk/AXonHwHxUogdXAWynaXsrx1i6dsgg+lOo/KJSF8oBAcprh1z5J3xgnPJD5hXxTL58F8s8onokdt0Q==",
"dependencies": {
"@babel/runtime": "^7.20.0",
"classnames": "^2.2.6",
"rc-resize-observer": "^1.0.0",
- "rc-util": "^5.15.0"
+ "rc-util": "^5.36.0"
},
"engines": {
"node": ">=8.x"
@@ -7300,6 +8110,30 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
+ "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
+ "node_modules/react-highlight-words": {
+ "version": "0.20.0",
+ "resolved": "https://registry.npmjs.org/react-highlight-words/-/react-highlight-words-0.20.0.tgz",
+ "integrity": "sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==",
+ "dependencies": {
+ "highlight-words-core": "^1.2.0",
+ "memoize-one": "^4.0.0",
+ "prop-types": "^15.5.8"
+ },
+ "peerDependencies": {
+ "react": "^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -7356,38 +8190,11 @@
"react": ">=16"
}
},
- "node_modules/react-markdown/node_modules/comma-separated-tokens": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
- "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/react-markdown/node_modules/property-information": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
- "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/react-markdown/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
- "node_modules/react-markdown/node_modules/space-separated-tokens": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
- "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
"node_modules/react-syntax-highlighter": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
@@ -7419,6 +8226,25 @@
"react": ">=0.14.1"
}
},
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
+ "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "globalthis": "^1.0.3",
+ "which-builtin-type": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/refractor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
@@ -7442,18 +8268,18 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/regexp.prototype.flags": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
- "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
+ "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
- "functions-have-names": "^1.2.3"
+ "set-function-name": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -7513,17 +8339,22 @@
"throttleit": "^1.0.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": {
- "version": "1.22.2",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
- "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dependencies": {
- "is-core-module": "^2.11.0",
+ "is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -7583,68 +8414,27 @@
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/run-applescript": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
- "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
- "dependencies": {
- "execa": "^5.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/run-applescript/node_modules/execa": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
- "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^6.0.0",
- "human-signals": "^2.1.0",
- "is-stream": "^2.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^4.0.1",
- "onetime": "^5.1.2",
- "signal-exit": "^3.0.3",
- "strip-final-newline": "^2.0.0"
+ "glob": "^7.1.3"
},
- "engines": {
- "node": ">=10"
+ "bin": {
+ "rimraf": "bin.js"
},
"funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/run-applescript/node_modules/get-stream": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "engines": {
- "node": ">=10"
+ "node_modules/rollup": {
+ "version": "2.78.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.0.tgz",
+ "integrity": "sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==",
+ "bin": {
+ "rollup": "dist/bin/rollup"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/run-applescript/node_modules/human-signals": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
- "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"engines": {
- "node": ">=10.17.0"
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
}
},
"node_modules/run-parallel": {
@@ -7689,6 +8479,23 @@
"node": ">=6"
}
},
+ "node_modules/safe-array-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
+ "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -7753,18 +8560,23 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/scmp": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz",
+ "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
+ },
"node_modules/scroll-into-view-if-needed": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz",
- "integrity": "sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+ "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/semver": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
- "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -7775,22 +8587,6 @@
"node": ">=10"
}
},
- "node_modules/semver/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/semver/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
- },
"node_modules/serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
@@ -7800,6 +8596,33 @@
"randombytes": "^2.1.0"
}
},
+ "node_modules/set-function-length": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+ "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
+ "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -7837,15 +8660,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/sigmund": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
- "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g=="
- },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
},
"node_modules/sister": {
"version": "3.0.2",
@@ -7910,18 +8729,18 @@
}
},
"node_modules/space-separated-tokens": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
- "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/sshpk": {
- "version": "1.17.0",
- "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
- "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+ "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"dev": true,
"dependencies": {
"asn1": "~0.2.3",
@@ -7943,6 +8762,30 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stacktrace-parser": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz",
+ "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==",
+ "dependencies": {
+ "type-fest": "^0.7.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/stacktrace-parser/node_modules/type-fest": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
+ "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -7977,17 +8820,18 @@
"dev": true
},
"node_modules/string.prototype.matchall": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
- "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==",
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
+ "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4",
- "get-intrinsic": "^1.1.3",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
"has-symbols": "^1.0.3",
- "internal-slot": "^1.0.3",
- "regexp.prototype.flags": "^1.4.3",
+ "internal-slot": "^1.0.5",
+ "regexp.prototype.flags": "^1.5.0",
+ "set-function-name": "^2.0.0",
"side-channel": "^1.0.4"
},
"funding": {
@@ -7995,13 +8839,13 @@
}
},
"node_modules/string.prototype.trim": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
- "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
+ "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"engines": {
"node": ">= 0.4"
@@ -8011,26 +8855,26 @@
}
},
"node_modules/string.prototype.trimend": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
- "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
+ "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/string.prototype.trimstart": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
- "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
+ "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
"dependencies": {
"call-bind": "^1.0.2",
- "define-properties": "^1.1.4",
- "es-abstract": "^1.20.4"
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -8081,6 +8925,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -8097,9 +8942,9 @@
}
},
"node_modules/style-to-object": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.2.tgz",
- "integrity": "sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==",
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz",
+ "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==",
"dependencies": {
"inline-style-parser": "0.1.1"
}
@@ -8131,6 +8976,69 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz",
"integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ=="
},
+ "node_modules/supertokens-auth-react": {
+ "version": "0.34.2",
+ "resolved": "https://registry.npmjs.org/supertokens-auth-react/-/supertokens-auth-react-0.34.2.tgz",
+ "integrity": "sha512-0Gaqb7SWL5+UXd9Ft87db3CK0i0CSzb9Ch3Lf+ZMcMfqeokWLZewhu3yqoZYvX035owjhMc72PYF1fxd6TwIQQ==",
+ "dependencies": {
+ "intl-tel-input": "^17.0.19",
+ "prop-types": "*",
+ "supertokens-js-override": "^0.0.4"
+ },
+ "engines": {
+ "node": ">=16.0.0",
+ "npm": ">=8"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0",
+ "supertokens-web-js": "^0.7.2"
+ }
+ },
+ "node_modules/supertokens-js-override": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/supertokens-js-override/-/supertokens-js-override-0.0.4.tgz",
+ "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg=="
+ },
+ "node_modules/supertokens-node": {
+ "version": "15.2.1",
+ "resolved": "https://registry.npmjs.org/supertokens-node/-/supertokens-node-15.2.1.tgz",
+ "integrity": "sha512-3zJ2EsiHYJHnYwAzDQI5Alp+4x/KcwEOBgeoPN5bWglZY0Xw0AzcZvd8S3N71vjLGvab0J3XxWmHeEHqSz5dbg==",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "cookie": "0.4.0",
+ "cross-fetch": "^3.1.6",
+ "debug": "^4.3.3",
+ "inflation": "^2.0.0",
+ "jose": "^4.13.1",
+ "libphonenumber-js": "^1.9.44",
+ "nodemailer": "^6.7.2",
+ "pkce-challenge": "^3.0.0",
+ "psl": "1.8.0",
+ "supertokens-js-override": "^0.0.4",
+ "twilio": "^4.7.2"
+ }
+ },
+ "node_modules/supertokens-web-js": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/supertokens-web-js/-/supertokens-web-js-0.7.3.tgz",
+ "integrity": "sha512-HkpIwQ7KCcD8gn3D9u9hfLV/rAf5sr2M6ICH16R3JV2TOB7XmFgX+jzVn3R7DuSRSXu39dT0nn0Vcdc30EzBbA==",
+ "peer": true,
+ "dependencies": {
+ "supertokens-js-override": "0.0.4",
+ "supertokens-website": "^17.0.1"
+ }
+ },
+ "node_modules/supertokens-website": {
+ "version": "17.0.4",
+ "resolved": "https://registry.npmjs.org/supertokens-website/-/supertokens-website-17.0.4.tgz",
+ "integrity": "sha512-ayWhEFvspUe26YhM1bq11ssEpnFCZIsoHZtJwJHgHsoflfMUKdgrzOix/bboI0PWJeNTUphHyZebw0ApctaS1Q==",
+ "peer": true,
+ "dependencies": {
+ "browser-tabs-lock": "^1.3.0",
+ "supertokens-js-override": "^0.0.4"
+ }
+ },
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -8157,10 +9065,11 @@
}
},
"node_modules/swr": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
- "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz",
+ "integrity": "sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==",
"dependencies": {
+ "client-only": "^0.0.1",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
@@ -8175,21 +9084,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/synckit": {
- "version": "0.8.5",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
- "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
- "dependencies": {
- "@pkgr/utils": "^2.3.1",
- "tslib": "^2.5.0"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/unts"
- }
- },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -8199,9 +9093,9 @@
}
},
"node_modules/terser": {
- "version": "5.19.2",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
- "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
+ "version": "5.24.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
+ "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@@ -8303,17 +9197,6 @@
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
- "node_modules/titleize": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
- "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -8343,18 +9226,34 @@
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/tough-cookie": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
- "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
+ "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
"dev": true,
"dependencies": {
- "psl": "^1.1.28",
- "punycode": "^2.1.1"
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
},
"engines": {
- "node": ">=0.8"
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -8385,9 +9284,9 @@
}
},
"node_modules/tslib": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
- "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -8426,6 +9325,32 @@
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"dev": true
},
+ "node_modules/twilio": {
+ "version": "4.19.0",
+ "resolved": "https://registry.npmjs.org/twilio/-/twilio-4.19.0.tgz",
+ "integrity": "sha512-4tM1LNM5LeUvnko4kIqIreY6vmjIo5Ag5jMEhjTDPj+GES82MnkfSkJv8N1k5/ZmeSvIdk5hjI87GB/DpDDePQ==",
+ "dependencies": {
+ "axios": "^0.26.1",
+ "dayjs": "^1.11.9",
+ "https-proxy-agent": "^5.0.0",
+ "jsonwebtoken": "^9.0.0",
+ "qs": "^6.9.4",
+ "scmp": "^2.1.0",
+ "url-parse": "^1.5.9",
+ "xmlbuilder": "^13.0.2"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
+ "node_modules/twilio/node_modules/axios": {
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+ "dependencies": {
+ "follow-redirects": "^1.14.8"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8461,6 +9386,54 @@
"node": ">= 0.6"
}
},
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
+ "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
+ "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/typed-array-length": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
@@ -8500,6 +9473,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ },
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
@@ -8616,9 +9594,9 @@
}
},
"node_modules/universalify": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
- "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
@@ -8628,14 +9606,15 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
"integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
- "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+ "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"funding": [
{
"type": "opencollective",
@@ -8669,6 +9648,15 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
@@ -8679,6 +9667,7 @@
},
"node_modules/usehooks-ts": {
"version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.9.1.tgz",
"integrity": "sha512-2FAuSIGHlY+apM9FVlj8/oNhd+1y+Uwv5QNkMQz1oSfdHk4PXo1qoCw9I5M7j0vpH8CSWFJwXbVPeYDjLCx9PA==",
"engines": {
"node": ">=16.15.0",
@@ -8690,10 +9679,13 @@
}
},
"node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "dev": true,
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -8715,6 +9707,14 @@
"node": ">=8"
}
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -8770,10 +9770,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
"node_modules/webpack": {
- "version": "5.88.2",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
- "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
+ "version": "5.89.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
+ "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -8821,7 +9826,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
- "peer": true,
"engines": {
"node": ">=10.13.0"
}
@@ -8848,6 +9852,15 @@
"node": ">=4.0"
}
},
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8877,17 +9890,55 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-builtin-type": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+ "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+ "dependencies": {
+ "function.prototype.name": "^1.1.5",
+ "has-tostringtag": "^1.0.0",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.0.5",
+ "is-finalizationregistry": "^1.0.2",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.1.4",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
+ "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+ "dependencies": {
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-weakmap": "^2.0.1",
+ "is-weakset": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/which-typed-array": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
- "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
+ "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
"dependencies": {
"available-typed-arrays": "^1.0.5",
- "call-bind": "^1.0.2",
+ "call-bind": "^1.0.4",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
- "has-tostringtag": "^1.0.0",
- "is-typed-array": "^1.1.10"
+ "has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -8918,6 +9969,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
+ "node_modules/xmlbuilder": {
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz",
+ "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -8927,9 +9986,9 @@
}
},
"node_modules/yallist": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
- "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yauzl": {
"version": "2.10.0",
diff --git a/agenta-web/package.json b/agenta-web/package.json
index f5d07c52e4..1d771dd9fa 100644
--- a/agenta-web/package.json
+++ b/agenta-web/package.json
@@ -10,44 +10,63 @@
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"test": "cypress run",
- "format": "prettier --check",
- "format-fix": "prettier --write ."
+ "format": "prettier --check .",
+ "format-fix": "prettier --write .",
+ "types:check": "tsc",
+ "types:watch": "tsc -w"
},
"dependencies": {
"@ant-design/colors": "^7.0.0",
"@ant-design/icons": "^5.0.1",
+ "@dnd-kit/core": "^6.1.0",
+ "@dnd-kit/sortable": "^8.0.0",
"@heroicons/react": "^2.0.17",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
+ "@monaco-editor/react": "^4.5.2",
"@next/mdx": "^13.4.13",
+ "@sentry/nextjs": "^7.77.0",
+ "@types/js-beautify": "^1.14.0",
+ "@types/lodash": "^4.14.201",
"@types/mdx": "^2.0.6",
- "@types/node": "18.16.3",
- "@types/react": "18.2.0",
"@types/react-dom": "18.2.1",
+ "@types/react-highlight-words": "^0.16.4",
+ "@types/react-syntax-highlighter": "^15.5.7",
+ "@types/uuid": "^9.0.7",
"ag-grid-community": "^30.0.6",
"ag-grid-react": "^30.0.3",
"antd": "^5.4.7",
"autoprefixer": "10.4.14",
"axios": "^1.4.0",
"classnames": "^2.3.2",
+ "dotenv": "^16.3.1",
"eslint": "8.39.0",
"eslint-config-next": "13.3.4",
+ "jotai": "^2.5.0",
"js-beautify": "^1.14.8",
+ "lodash": "^4.17.21",
"next": "13.3.4",
+ "nextjs-cors": "^2.1.2",
"postcss": "8.4.23",
+ "posthog-js": "^1.87.2",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-error-boundary": "^4.0.11",
+ "react-highlight-words": "^0.20.0",
"react-jss": "^10.10.0",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-youtube": "^10.1.0",
+ "supertokens-auth-react": "^0.34.0",
+ "supertokens-node": "^15.0.4",
"swr": "^2.1.5",
"typescript": "5.0.4",
- "usehooks-ts": "^2.9.1"
+ "usehooks-ts": "^2.9.1",
+ "uuid": "^9.0.1"
},
"devDependencies": {
- "cypress": "^12.11.0",
- "eslint-config-prettier": "^8.8.0",
+ "@types/node": "^20.8.10",
+ "cypress": "^13.4.0",
"node-mocks-http": "^1.12.2",
"prettier": "^3.0.0"
}
diff --git a/agenta-web/prod.Dockerfile b/agenta-web/prod.Dockerfile
new file mode 100644
index 0000000000..7841bef8ff
--- /dev/null
+++ b/agenta-web/prod.Dockerfile
@@ -0,0 +1,22 @@
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Install only production dependencies
+COPY package.json package-lock.json* ./
+RUN npm ci --omit=dev
+
+# Copy only necessary files
+COPY src ./src
+COPY public ./public
+COPY next.config.js .
+COPY tsconfig.json .
+COPY postcss.config.js .
+COPY .env .
+# used in cloud
+COPY sentry.* .
+# Build the Next.js app for production
+RUN npm run build
+
+# Start the production server
+CMD ["npm", "start"]
diff --git a/agenta-web/prod.gh.Dockerfile b/agenta-web/prod.gh.Dockerfile
new file mode 100644
index 0000000000..76c48e6bc0
--- /dev/null
+++ b/agenta-web/prod.gh.Dockerfile
@@ -0,0 +1,21 @@
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Install only production dependencies
+COPY package.json package-lock.json* ./
+RUN npm ci --omit=dev
+
+# Copy only necessary files
+COPY src ./src
+COPY public ./public
+COPY next.config.js .
+COPY tsconfig.json .
+COPY postcss.config.js .
+# used in cloud
+COPY sentry.* .
+# Build the Next.js app for production
+RUN npm run build
+
+# Start the production server
+CMD ["npm", "start"]
diff --git a/agenta-web/public/assets/complex-img.png b/agenta-web/public/assets/complex-img.png
new file mode 100644
index 0000000000..d25bd4fee0
Binary files /dev/null and b/agenta-web/public/assets/complex-img.png differ
diff --git a/agenta-web/public/assets/dark-logo.svg b/agenta-web/public/assets/dark-logo.svg
new file mode 100644
index 0000000000..6cb8ef3330
--- /dev/null
+++ b/agenta-web/public/assets/dark-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/agenta-web/public/assets/light-logo.svg b/agenta-web/public/assets/light-logo.svg
new file mode 100644
index 0000000000..9c795f8e88
--- /dev/null
+++ b/agenta-web/public/assets/light-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/agenta-web/public/assets/simple-img.png b/agenta-web/public/assets/simple-img.png
new file mode 100644
index 0000000000..3fb9f338da
Binary files /dev/null and b/agenta-web/public/assets/simple-img.png differ
diff --git a/agenta-web/public/assets/slack.png b/agenta-web/public/assets/slack.png
new file mode 100644
index 0000000000..9ded7efaae
Binary files /dev/null and b/agenta-web/public/assets/slack.png differ
diff --git a/agenta-web/src/_app.tsx b/agenta-web/src/_app.tsx
new file mode 100644
index 0000000000..e03e20b16c
--- /dev/null
+++ b/agenta-web/src/_app.tsx
@@ -0,0 +1,14 @@
+import "@/styles/globals.css"
+import type {AppProps} from "next/app"
+import Layout from "@/components/Layout/Layout"
+import ThemeContextProvider from "@/components/Layout/ThemeContextProvider"
+
+export default function App({Component, pageProps}: AppProps) {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/agenta-web/src/code_snippets/testsets/create_with_upload/curl.ts b/agenta-web/src/code_snippets/testsets/create_with_upload/curl.ts
index f7b4ae7381..ca5f7f806a 100644
--- a/agenta-web/src/code_snippets/testsets/create_with_upload/curl.ts
+++ b/agenta-web/src/code_snippets/testsets/create_with_upload/curl.ts
@@ -1,8 +1,8 @@
-export default function cURLCode(uri: string, appName: string): string {
+export default function cURLCode(uri: string, appId: string): string {
return `curl -X POST ${uri} \\
-H 'Content-Type: multipart/form-data' \\
-F 'file=@/path/to/your/file.csv' \\
-F 'testset_name=your_testset_name' \\
--F 'app_name=${appName}'
+-F 'app_id=${appId}'
`
}
diff --git a/agenta-web/src/code_snippets/testsets/create_with_upload/python.ts b/agenta-web/src/code_snippets/testsets/create_with_upload/python.ts
index 7f41465a2c..937fb0b709 100644
--- a/agenta-web/src/code_snippets/testsets/create_with_upload/python.ts
+++ b/agenta-web/src/code_snippets/testsets/create_with_upload/python.ts
@@ -1,14 +1,14 @@
-export default function pythonCode(uri: string, appName: string): string {
+export default function pythonCode(uri: string, appId: string): string {
return `import requests
url = '${uri}'
file_path = '/path/to/your/file.csv'
testset_name = 'your_testset_name'
-appName = '${appName}'
+appId = '${appId}'
with open(file_path, 'rb') as file:
files = {'file': file}
- data = {'testset_name': testset_name, 'app_name': appName}
+ data = {'testset_name': testset_name, 'app_id': appId}
response = requests.post(url, files=files, data=data)
print(response.status_code)
diff --git a/agenta-web/src/code_snippets/testsets/create_with_upload/typescript.ts b/agenta-web/src/code_snippets/testsets/create_with_upload/typescript.ts
index 47623c6edf..8cabc21d4e 100644
--- a/agenta-web/src/code_snippets/testsets/create_with_upload/typescript.ts
+++ b/agenta-web/src/code_snippets/testsets/create_with_upload/typescript.ts
@@ -1,6 +1,6 @@
import {js as beautify} from "js-beautify"
-export default function tsCode(uri: string, appName: string): string {
+export default function tsCode(uri: string, appId: string): string {
const codeString = `import axios from 'axios';
const fs = require('fs');
const FormData = require('form-data');
@@ -8,12 +8,12 @@ export default function tsCode(uri: string, appName: string): string {
const url = '${uri}';
const filePath = './cypress/data/countries-genders.csv';
const testsetName = 'tribalafa';
- const appName = '${appName}';
+ const appId = '${appId}';
const formData = new FormData();
formData.append('file', fs.createReadStream(filePath));
formData.append('testset_name', testsetName);
- formData.append('app_name', appName);
+ formData.append('app_id', appId);
const config = {
headers: {
diff --git a/agenta-web/src/components/AlertPopup/AlertPopup.tsx b/agenta-web/src/components/AlertPopup/AlertPopup.tsx
index 5d9e56171d..e405394498 100644
--- a/agenta-web/src/components/AlertPopup/AlertPopup.tsx
+++ b/agenta-web/src/components/AlertPopup/AlertPopup.tsx
@@ -1,6 +1,7 @@
import React, {ReactNode} from "react"
import {Modal, ModalFuncProps} from "antd"
import {ExclamationCircleOutlined} from "@ant-design/icons"
+import {globalErrorHandler} from "@/lib/helpers/errorHandler"
function handleCb(cb: AlertPopupProps["onOk"]) {
if (typeof cb !== "function") return cb
@@ -8,7 +9,7 @@ function handleCb(cb: AlertPopupProps["onOk"]) {
const res = cb()
if (res instanceof Promise) {
return new Promise((_res) => {
- res.catch(console.error).finally(() => _res(undefined))
+ res.catch(globalErrorHandler).finally(() => _res(undefined))
})
}
return res
diff --git a/agenta-web/src/components/AppSelector/AppCard.tsx b/agenta-web/src/components/AppSelector/AppCard.tsx
index 890e1adec7..a05652cc60 100644
--- a/agenta-web/src/components/AppSelector/AppCard.tsx
+++ b/agenta-web/src/components/AppSelector/AppCard.tsx
@@ -1,10 +1,55 @@
-import {Modal, message, Card, Avatar} from "antd"
+import {Modal, Card, Avatar} from "antd"
import {DeleteOutlined} from "@ant-design/icons"
import {removeApp} from "@/lib/services/api"
-import useSWR, {mutate} from "swr"
import {useState} from "react"
import Link from "next/link"
import {renameVariablesCapitalizeAll} from "@/lib/helpers/utils"
+import {createUseStyles} from "react-jss"
+import {getGradientFromStr} from "@/lib/helpers/colors"
+import {ListAppsItem} from "@/lib/Types"
+import {useProfileData, Role} from "@/contexts/profile.context"
+import {useAppsData} from "@/contexts/app.context"
+
+const useStyles = createUseStyles({
+ card: {
+ width: 300,
+ height: 120,
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "space-between",
+ overflow: "hidden",
+ "& svg": {
+ color: "red",
+ },
+ "& .ant-card-meta": {
+ height: "90%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ "& .ant-card-meta-title div": {
+ textAlign: "center",
+ },
+ },
+ cardCover: {
+ "z-index": 1,
+ position: "absolute",
+ top: 0,
+ right: 0,
+ left: 0,
+ background: "transparent",
+ margin: "auto",
+ width: "300px",
+ height: "70px",
+ display: "flex",
+ overflow: "hidden",
+ "flex-direction": "column",
+ "justify-content": "space-between",
+ },
+ cardLink: {
+ padding: "24px",
+ },
+})
const DeleteModal: React.FC<{
open: boolean
@@ -29,12 +74,14 @@ const DeleteModal: React.FC<{
}
const AppCard: React.FC<{
- appName: string
- key: number
- index: number
-}> = ({appName, index}) => {
+ app: ListAppsItem
+}> = ({app}) => {
const [visibleDelete, setVisibleDelete] = useState(false)
- const [confirmLoading, setConfirmLoading] = useState(false) // add this line
+ const [confirmLoading, setConfirmLoading] = useState(false)
+ const {role} = useProfileData()
+ const isOwner = role === Role.OWNER
+ const {mutate} = useAppsData()
+
const showDeleteModal = () => {
setVisibleDelete(true)
}
@@ -42,11 +89,10 @@ const AppCard: React.FC<{
const handleDeleteOk = async () => {
setConfirmLoading(true)
try {
- await removeApp(appName)
- // Refresh the data (if you're using SWR or a similar library)
- mutate(`${process.env.NEXT_PUBLIC_AGENTA_API_URL}/api/app_variant/list_apps/`)
+ await removeApp(app.app_id)
+ mutate()
} catch (error) {
- message.error(`Failed to remove app: ${error.message}`)
+ console.error(error)
} finally {
setVisibleDelete(false)
setConfirmLoading(false)
@@ -55,61 +101,28 @@ const AppCard: React.FC<{
const handleDeleteCancel = () => {
setVisibleDelete(false)
}
- const gradients = [
- "linear-gradient(to bottom right, #424242, #9F1239, #560BAD)",
- "linear-gradient(to bottom right, #C6F6D5, #34D399, #3B82F6)",
- "linear-gradient(to bottom right, #FEEBC8, #F59E0B, #9A3412)",
- "linear-gradient(to bottom right, #C6F6D5, #22D3EE, #7137F1)",
- "linear-gradient(to bottom right, #BFDBFE, #60A5FA, #3B82F6)",
- "linear-gradient(to bottom right, #8B5CF6, #FDE047)",
- "linear-gradient(to bottom right, #B91C1C, #D97706, #F59E0B)",
- "linear-gradient(to bottom right, #93C5FD, #C6F6D5, #FDE047)",
- "linear-gradient(to bottom right, #3B82F6, #1D4ED8, #111827)",
- "linear-gradient(to bottom right, #34D399, #A78BFA)",
- "linear-gradient(to bottom right, #FEEBC8, #F9A8D4, #F43F5E)",
- "linear-gradient(to bottom right, #10B981, #047857)",
- "linear-gradient(to bottom right, #F472B6, #D946EF, #4F46E5)",
- "linear-gradient(to bottom right, #60A5FA, #3B82F6)",
- ]
+
+ const classes = useStyles()
return (
<>
,
- ]}
+ className={classes.card}
+ actions={
+ isOwner
+ ? [ ]
+ : undefined
+ }
>
-
+
- {renameVariablesCapitalizeAll(appName)}
-
- }
+ title={{renameVariablesCapitalizeAll(app.app_name)}
}
avatar={
- {appName.charAt(0).toUpperCase()}
+ {app.app_name.charAt(0).toUpperCase()}
}
/>
@@ -120,7 +133,7 @@ const AppCard: React.FC<{
open={visibleDelete}
handleOk={handleDeleteOk}
handleCancel={handleDeleteCancel}
- appName={appName}
+ appName={app.app_name}
confirmLoading={confirmLoading}
/>
>
diff --git a/agenta-web/src/components/AppSelector/AppSelector.tsx b/agenta-web/src/components/AppSelector/AppSelector.tsx
index f1ffc90fed..72d3c0cdd5 100644
--- a/agenta-web/src/components/AppSelector/AppSelector.tsx
+++ b/agenta-web/src/components/AppSelector/AppSelector.tsx
@@ -1,25 +1,41 @@
-import {useState, useEffect} from "react"
+import {useState, useEffect, useMemo} from "react"
import {useRouter} from "next/router"
+import {usePostHog} from "posthog-js/react"
import {PlusOutlined} from "@ant-design/icons"
import {Input, Modal, ConfigProvider, theme, Spin, Card, Button, notification, Divider} from "antd"
import AppCard from "./AppCard"
-import {Template, AppTemplate, TemplateImage} from "@/lib/Types"
+import {Template, GenericObject} from "@/lib/Types"
import {useAppTheme} from "../Layout/ThemeContextProvider"
import {CloseCircleFilled} from "@ant-design/icons"
import TipsAndFeatures from "./TipsAndFeatures"
import Welcome from "./Welcome"
-import {isAppNameInputValid} from "@/lib/helpers/utils"
-import {fetchApps, getTemplates, pullTemplateImage, startTemplate} from "@/lib/services/api"
+import {getOpenAIKey, isAppNameInputValid, isDemo} from "@/lib/helpers/utils"
+import {
+ createAndStartTemplate,
+ getProfile,
+ getTemplates,
+ removeApp,
+ waitForAppToStart,
+} from "@/lib/services/api"
import AddNewAppModal from "./modals/AddNewAppModal"
import AddAppFromTemplatedModal from "./modals/AddAppFromTemplateModal"
+import MaxAppModal from "./modals/MaxAppModal"
import WriteOwnAppModal from "./modals/WriteOwnAppModal"
import {createUseStyles} from "react-jss"
+import {useAppsData} from "@/contexts/app.context"
+import {useProfileData} from "@/contexts/profile.context"
+import CreateAppStatusModal from "./modals/CreateAppStatusModal"
type StyleProps = {
themeMode: "dark" | "light"
}
const useStyles = createUseStyles({
+ container: ({themeMode}: StyleProps) => ({
+ marginTop: 10,
+ width: "100%",
+ color: themeMode === "dark" ? "#fff" : "#000",
+ }),
cardsList: ({themeMode}: StyleProps) => ({
display: "flex",
flexWrap: "wrap",
@@ -33,33 +49,88 @@ const useStyles = createUseStyles({
backgroundColor: "#1777FF",
borderColor: "#1777FF !important",
color: "#FFFFFF",
+ width: 300,
+ height: 120,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ cursor: "pointer",
"& .ant-card-meta-title": {
color: "#FFFFFF",
},
},
+ createCardMeta: {
+ height: "90%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-evenly",
+ },
+ closeIcon: {
+ fontSize: 20,
+ color: "red",
+ },
+ divider: ({themeMode}: StyleProps) => ({
+ marginTop: 0,
+ borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.15)",
+ }),
+ h1: {
+ fontSize: 24,
+ },
+ modal: {
+ "& .ant-modal-body": {
+ display: "flex",
+ flexDirection: "column",
+ gap: "0.75rem",
+ marginTop: 20,
+ },
+ },
+ modalError: {
+ color: "red",
+ marginLeft: "10px",
+ },
+ modalBtn: {
+ alignSelf: "flex-end",
+ },
})
+const timeout = isDemo() ? 60000 : 30000
+
const AppSelector: React.FC = () => {
const router = useRouter()
+ const posthog = usePostHog()
const {appTheme} = useAppTheme()
const classes = useStyles({themeMode: appTheme} as StyleProps)
const [isCreateAppModalOpen, setIsCreateAppModalOpen] = useState(false)
const [isCreateAppFromTemplateModalOpen, setIsCreateAppFromTemplateModalOpen] = useState(false)
const [isWriteAppModalOpen, setIsWriteAppModalOpen] = useState(false)
+ const [isMaxAppModalOpen, setIsMaxAppModalOpen] = useState(false)
const [templates, setTemplates] = useState([])
const [templateMessage, setTemplateMessage] = useState("")
- const [templateName, setTemplateName] = useState(undefined)
+ const [templateId, setTemplateId] = useState(undefined)
const [isInputTemplateModalOpen, setIsInputTemplateModalOpen] = useState(false)
+ const [statusModalOpen, setStatusModalOpen] = useState(false)
const [fetchingTemplate, setFetchingTemplate] = useState(false)
- const [appNameExist, setAppNameExist] = useState(false)
const [newApp, setNewApp] = useState("")
+ const {selectedOrg} = useProfileData()
+ const {apps, error, isLoading, mutate} = useAppsData()
+ const [statusData, setStatusData] = useState<{status: string; details?: any; appId?: string}>({
+ status: "",
+ details: undefined,
+ appId: undefined,
+ })
- const showCreateAppModal = () => {
+ const trackingEnabled = process.env.NEXT_PUBLIC_TELEMETRY_TRACKING_ENABLED === "true"
+ const showCreateAppModal = async () => {
setIsCreateAppModalOpen(true)
}
+ const showMaxAppError = () => {
+ setIsMaxAppModalOpen(true)
+ }
const showCreateAppFromTemplateModal = () => {
+ setTemplateId(undefined)
+ setNewApp("")
setIsCreateAppModalOpen(false)
setIsCreateAppFromTemplateModalOpen(true)
}
@@ -89,23 +160,10 @@ const AppSelector: React.FC = () => {
const handleInputTemplateModalCancel = () => {
if (fetchingTemplate) return
setIsInputTemplateModalOpen(false)
- setNewApp("")
- setTemplateName(undefined)
- }
-
- const handleNavigation = () => {
- notification.success({
- message: "Template Selection",
- description:
- "Once your app is up and running, you'll be redirected to the app playground.",
- duration: 9,
- })
- setTimeout(() => {
- router.push(`/apps/${newApp}/playground`)
- }, 10000)
}
useEffect(() => {
+ if (!isLoading) mutate()
const fetchTemplates = async () => {
const data = await getTemplates()
if (typeof data == "object") {
@@ -118,103 +176,88 @@ const AppSelector: React.FC = () => {
fetchTemplates()
}, [])
- const fetchTemplateImage = async (image_name: string) => {
- const response = await pullTemplateImage(image_name)
- return response
- }
-
- const retrieveOpenAIKey = () => {
- const apiKey = localStorage.getItem("openAiToken")
- return apiKey
- }
+ const handleTemplateCardClick = async (template_id: string) => {
+ handleInputTemplateModalCancel()
+ handleCreateAppFromTemplateModalCancel()
+ handleCreateAppModalCancel()
- const createAppVariantFromTemplateImage = async (
- app_name: string,
- image_id: string,
- image_tag: string,
- api_key: string,
- ) => {
- const variantData: AppTemplate = {
- app_name: app_name,
- image_id: image_id,
- image_tag: image_tag,
- env_vars: {
- OPENAI_API_KEY: api_key,
- },
- }
- const response = await startTemplate(variantData)
- if (response.status == 200) {
- notification.success({
- message: "Template Selection",
- description: "App has been created and will begin to run.",
- duration: 5,
- })
- return response
- } else {
+ // warn the user and redirect if openAI key is not present
+ const openAIKey = getOpenAIKey()
+ if (!openAIKey && !isDemo()) {
notification.error({
- message: "Template Selection",
- description: "An error occured when trying to start the variant.",
+ message: "OpenAI API Key Missing",
+ description: "Please provide your OpenAI API key to access this feature.",
duration: 5,
})
- setFetchingTemplate(false)
+ router.push("/settings?tab=secrets")
return
}
- }
- const handleTemplateCardClick = async (image_name: string) => {
setFetchingTemplate(true)
+ setStatusModalOpen(true)
- const OpenAIKey = retrieveOpenAIKey() as string
- if (OpenAIKey === null) {
- notification.error({
- message: "OpenAI API Key Missing",
- description: "Please provide your OpenAI API key to access this feature.",
- duration: 5,
- })
- router.push("/apikeys")
- return
- }
+ // attempt to create and start the template, notify user of the progress
+ await createAndStartTemplate({
+ appName: newApp,
+ templateId: template_id,
+ orgId: selectedOrg?.id!,
+ openAIKey: isDemo() ? "" : (openAIKey as string),
+ timeout,
+ onStatusChange: (status, details, appId) => {
+ setStatusData((prev) => ({status, details, appId: appId || prev.appId}))
+ if (["error", "bad_request", "timeout", "success"].includes(status))
+ setFetchingTemplate(false)
+ if (status === "success") {
+ mutate()
- notification.info({
- message: "Template Selection",
- description: "Fetching template image...",
- duration: 10,
+ if (trackingEnabled) {
+ // Get user profile
+ getProfile().then((res) => {
+ // Update distinct_id and track successfully app variant deployment
+ posthog?.identify(res?.data?.id)
+ posthog?.capture("app_deployment", {
+ properties: {
+ app_id: appId,
+ environment: "UI",
+ deployed_by: res?.data?.id,
+ },
+ })
+ })
+ }
+ }
+ },
})
+ }
- const data: TemplateImage = await fetchTemplateImage(image_name)
- if (data.message) {
- notification.error({
- message: "Template Selection",
- description: `${data.message}!`,
- duration: 10,
- })
- setFetchingTemplate(false)
- } else {
- notification.info({
- message: "Template Section",
- description: "Creating variant from template image...",
- duration: 15,
- })
- await createAppVariantFromTemplateImage(
- newApp,
- data.image_id,
- data.image_tag,
- OpenAIKey,
- ).finally(() => {
- handleCreateAppFromTemplateModalCancel()
- handleCreateAppModalCancel()
- handleNavigation()
- })
+ const onErrorRetry = async () => {
+ if (statusData.appId) {
+ setStatusData((prev) => ({...prev, status: "cleanup", details: undefined}))
+ await removeApp(statusData.appId).catch(console.error)
+ mutate()
}
+ handleTemplateCardClick(templateId as string)
}
- const {data, error, isLoading} = fetchApps()
-
- useEffect(() => {
- if (data) {
- setAppNameExist(data.some((app) => app.app_name === newApp))
+ const onTimeoutRetry = async () => {
+ if (!statusData.appId) return
+ setStatusData((prev) => ({...prev, status: "starting_app", details: undefined}))
+ try {
+ await waitForAppToStart({appId: statusData.appId, timeout})
+ } catch (error: any) {
+ if (error.message === "timeout") {
+ setStatusData((prev) => ({...prev, status: "timeout", details: undefined}))
+ } else {
+ setStatusData((prev) => ({...prev, status: "error", details: error}))
+ }
}
- }, [data, newApp])
+ setStatusData((prev) => ({...prev, status: "success", details: undefined}))
+ mutate()
+ }
+
+ const appNameExist = useMemo(
+ () => apps.some((app: GenericObject) => app.app_name === newApp),
+ [apps, newApp],
+ )
return (
{
algorithm: appTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
}}
>
-
+
{isLoading ? (
-
+
loading...
) : error ? (
-
-
+
+
failed to load
- ) : Array.isArray(data) && data.length ? (
+ ) : Array.isArray(apps) && apps.length ? (
<>
-
LLM Applications
-
+
LLM Applications
+
- {Array.isArray(data) && (
+ {Array.isArray(apps) && (
<>
- {data.map((app: any, index: number) => (
+ {apps.map((app, index: number) => (
))}
{
+ if (isDemo() && apps.length > 2) {
+ showMaxAppError()
+ } else {
+ showCreateAppModal()
+ }
}}
- onClick={showCreateAppModal}
>
- Create New App
-
- }
+ data-cy="create-new-app-button"
+ className={classes.createCardMeta}
+ title={
Create New App
}
avatar={
}
/>
@@ -298,7 +312,10 @@ const AppSelector: React.FC = () => {
>
) : (
-
+
)}
@@ -316,22 +333,24 @@ const AppSelector: React.FC = () => {
noTemplateMessage={templateMessage}
onCardClick={(template) => {
showInputTemplateModal()
- setTemplateName(template.image.name)
+ setTemplateId(template.id)
+ }}
+ />
+
{
+ setIsMaxAppModalOpen(false)
}}
/>
{
onChange={(e) => setNewApp(e.target.value)}
disabled={fetchingTemplate}
/>
- {appNameExist && (
- App name already exist
- )}
+ {appNameExist && App name already exist
}
{newApp.length > 0 && !isAppNameInputValid(newApp) && (
-
+
App name must contain only letters, numbers, underscore, or dash
)}
{
if (appNameExist) {
notification.warning({
@@ -375,7 +394,7 @@ const AppSelector: React.FC = () => {
newApp.length > 0 &&
isAppNameInputValid(newApp)
) {
- handleTemplateCardClick(templateName)
+ handleTemplateCardClick(templateId as string)
} else {
notification.warning({
message: "Template Selection",
@@ -389,6 +408,15 @@ const AppSelector: React.FC = () => {
Create
+
setStatusModalOpen(false)}
+ statusData={statusData}
+ appName={newApp}
+ />
diff --git a/agenta-web/src/components/AppSelector/AppTemplateCard.tsx b/agenta-web/src/components/AppSelector/AppTemplateCard.tsx
index ddddcb0e46..befa02ca7b 100644
--- a/agenta-web/src/components/AppSelector/AppTemplateCard.tsx
+++ b/agenta-web/src/components/AppSelector/AppTemplateCard.tsx
@@ -1,7 +1,41 @@
import {Button, Card, Tag, Typography} from "antd"
+import {createUseStyles} from "react-jss"
+
+type StylesProp = {
+ tag: string | undefined
+}
const {Text} = Typography
+const useStyles = createUseStyles({
+ card: {
+ "& .ant-card-body": {
+ padding: "1rem",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-evenly",
+ flexDirection: "column",
+ position: "relative",
+ },
+ },
+ tag: {
+ position: "absolute",
+ right: 0,
+ top: 8,
+ },
+ text1: ({tag}: StylesProp) => ({
+ marginBottom: -4,
+ marginTop: tag ? 20 : 0,
+ fontSize: 15,
+ }),
+ link: {
+ textAlign: "center",
+ },
+ createBtn: {
+ width: "100%",
+ },
+})
+
interface Props {
title: string
onClick: () => void
@@ -11,28 +45,20 @@ interface Props {
}
const AppTemplateCard: React.FC = ({title, tag, onClick, body, noTemplate}) => {
+ const classes = useStyles({tag} as StylesProp)
return (
-
+
{tag && (
-
+
{tag}
)}
-
+
{title}
{noTemplate ? (
-
+
{body} here .
@@ -45,9 +71,8 @@ const AppTemplateCard: React.FC = ({title, tag, onClick, body, noTemplate
Create App
diff --git a/agenta-web/src/components/AppSelector/TipsAndFeatures.tsx b/agenta-web/src/components/AppSelector/TipsAndFeatures.tsx
index 5bdd60a271..1414cd6672 100644
--- a/agenta-web/src/components/AppSelector/TipsAndFeatures.tsx
+++ b/agenta-web/src/components/AppSelector/TipsAndFeatures.tsx
@@ -6,12 +6,55 @@ import {MDXProvider} from "@mdx-js/react"
import slide1 from "../../welcome-highlights/tip1.mdx"
import slide2 from "../../welcome-highlights/tip2.mdx"
+import {createUseStyles} from "react-jss"
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
+
+const useStyles = createUseStyles({
+ container: ({themeMode}: StyleProps) => ({
+ backgroundColor: themeMode === "dark" ? "#000" : "rgba(0,0,0,0.03)",
+ borderRadius: 10,
+ padding: 20,
+ width: "100%",
+ margin: "30px auto",
+ }),
+ header: {
+ "& svg": {
+ fontSize: 24,
+ color: "rgb(255, 217, 0)",
+ },
+ "& h1": {
+ margin: "8px 0",
+ },
+ },
+ dotsContainer: {
+ textAlign: "center",
+ marginBottom: "20px",
+ },
+ dots: {
+ display: "inline-block",
+ width: 10,
+ height: 10,
+ borderRadius: "50%",
+ margin: "0 5px",
+ cursor: "pointer",
+ },
+ mdxContainer: {
+ borderRadius: 10,
+ margin: "10px auto",
+ width: "100%",
+ lineHeight: 1.6,
+ },
+})
const slides: any[] = []
const TipsAndFeatures = () => {
const {appTheme} = useAppTheme()
const [activeIndex, setActiveIndex] = useState(0)
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
const handleDotClick = (index: number) => {
setActiveIndex(index)
@@ -38,46 +81,26 @@ const TipsAndFeatures = () => {
return (
<>
{slides.length ? (
-
-
-
- Highlights
+
+
+
+ Highlights
-
+
{slides.map((_, index) => (
handleDotClick(index)}
/>
))}
-
+
,
diff --git a/agenta-web/src/components/AppSelector/Welcome.tsx b/agenta-web/src/components/AppSelector/Welcome.tsx
index 0510a59f79..0d06a3bb91 100644
--- a/agenta-web/src/components/AppSelector/Welcome.tsx
+++ b/agenta-web/src/components/AppSelector/Welcome.tsx
@@ -1,21 +1,30 @@
-import {Button, Divider} from "antd"
+import {Tag} from "antd"
import React from "react"
import {useAppTheme} from "../Layout/ThemeContextProvider"
import {createUseStyles} from "react-jss"
+import {CheckCircleFilled, ClockCircleOutlined} from "@ant-design/icons"
type StyleProps = {
themeMode: "dark" | "light"
}
const useStyles = createUseStyles({
+ head: {
+ marginBottom: 30,
+ "& h2": {
+ fontSize: 18,
+ margin: "20px 0",
+ textAlign: "center",
+ },
+ },
heading: {
display: "flex",
alignItems: "center",
+ justifyContent: "center",
gap: "1rem",
-
"& > h1": {
margin: 0,
- fontSize: 42,
+ fontSize: 36,
},
"& > img": {
@@ -23,32 +32,6 @@ const useStyles = createUseStyles({
height: 44,
},
},
- h2: {
- fontSize: "24px",
- margin: "20px 0",
- },
- divider: ({themeMode}: StyleProps) => ({
- borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.15)",
- marginTop: 0,
- }),
- blueBox: ({themeMode}: StyleProps) => ({
- backgroundColor: themeMode === "dark" ? "rgb(24, 36, 58)" : "#e6f4ff",
- borderRadius: 10,
- padding: "1rem",
- "&> h3": {
- margin: 0,
- fontSize: 20,
- },
- }),
- description: {
- padding: "0 20px",
- lineHeight: 1.7,
- marginBottom: "2rem",
- "& > p:nth-of-type(2)": {
- fontWeight: 600,
- fontSize: 15,
- },
- },
"@keyframes wave": {
"0%": {
transform: "rotate(0deg)",
@@ -72,63 +55,165 @@ const useStyles = createUseStyles({
transform: "rotate(0deg)",
},
},
+ description: {
+ lineHeight: 1.7,
+ },
+ wrapper: {
+ display: "flex",
+ justifyContent: "space-between",
+ gap: 20,
+ maxWidth: "1250px",
+ margin: "0 auto",
+ width: "100%",
+ },
+ container: ({themeMode}: StyleProps) => ({
+ display: "flex",
+ justifyContent: "space-between",
+ cursor: "pointer",
+ flexDirection: "column",
+ border: `1px solid ${themeMode === "dark" ? "rgb(13, 17, 23)" : "#91caff"}`,
+ padding: "15px",
+ borderRadius: 10,
+ flex: 1,
+ backgroundColor: themeMode === "dark" ? "#000" : "#fff",
+ transition: "all 0.3s ease-out",
+ "&:hover": {
+ backgroundColor: themeMode === "dark" ? "" : "#f3faff",
+ boxShadow: themeMode === "dark" ? "0 0 10px rgba(225, 225, 225, 0.3)" : "",
+ },
+ }),
+ title: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: "15px",
+ "& h1": {
+ fontWeight: 600,
+ fontSize: 24,
+ },
+ },
+ tag: {
+ padding: "2px 6px",
+ fontWeight: "bold",
+ },
+ img: ({themeMode}: StyleProps) => ({
+ width: "100%",
+ filter: themeMode === "dark" ? "invert(1)" : "none",
+ }),
+ steps: ({themeMode}: StyleProps) => ({
+ fontSize: 16,
+ margin: "20px 0 0",
+ display: "flex",
+ flexDirection: "column",
+ listStyleType: "none",
+ padding: 20,
+ "& li": {
+ marginBottom: 10,
+ },
+ "& svg": {
+ color: themeMode === "dark" ? "#fff" : "#0958d9",
+ marginRight: 10,
+ },
+ "& span": {
+ fontWeight: 600,
+ },
+ }),
+ text: {
+ marginLeft: 25,
+ },
})
interface Props {
- onCreateAppClick: () => void
+ onWriteOwnApp: () => void
+ onCreateFromTemplate: () => void
}
-const Welcome: React.FC = ({onCreateAppClick}) => {
+const Welcome: React.FC = ({onWriteOwnApp, onCreateFromTemplate}) => {
const {appTheme} = useAppTheme()
const classes = useStyles({themeMode: appTheme} as StyleProps)
return (
-
-
-
-
Welcome to Agenta
-
-
-
The developer-first open source LLMOps platform.
-
-
-
-
- Agenta is an open-source developer first LLMOps platform to streamline the
- process of building LLM-powered applications. Building LLM-powered apps is an
- iterative process with lots of prompt-engineering and testing multiple variants.
-
- Agenta brings the CI/CD platform to this process by enabling you to quickly
- iterate, experiment, evaluate, and optimize your LLM apps. All without imposing
- any restrictions on your choice of framework, library, or model.
-
-
+ <>
+
+
+
+
Welcome to Agenta
+
+
+ The developer-first open source LLMOps platform.
+
+
+
+
+
Quickstart From a Template
+
+
+
-
- Read{" "}
-
- Documentation
- {" "}
- on how to get started.
-
-
-
-
Get started creating your first LLM App
+
+
+ Start from a template
+
+
+ Compare prompts and models
+
+
+ Create testsets
+
+
+ Evaluate outputs
+
+
+ Deploy in one click
+
+
+
+
+
+
+
Build Complex LLM apps
+
-
- This guide assumes you have completed the installation process. If not, please
- follow our{" "}
-
- installation guide
-
- .
-
+
+
-
- Create New App
-
-
-
+
+
+ Start from code
+
+
+ Use Langchain ,{" "}
+ Llama Index , or any framework
+
+
+ Use OpenAI , Cohere ,
+ or self-hosted open-source models
+
+
+ Continue in the UI: Everything in
+ the left
+
+
+ Streamline collaboration between
+ devs and domain experts!
+
+
+
+
+
+ >
)
}
diff --git a/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx b/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx
index 5f26dab271..94e1db3d9b 100644
--- a/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx
+++ b/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx
@@ -45,6 +45,7 @@ const AddAppFromTemplatedModal: React.FC = ({
return (
= ({
onClick={() => {
onCardClick(template)
}}
- tag={template.image.architecture}
+
+ // commented to remove the tag amd64 tag
+ // tag={template.image.architecture}
/>
))
diff --git a/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx b/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx
index 8229395d17..c40e5778ef 100644
--- a/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx
+++ b/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx
@@ -63,6 +63,7 @@ const AddNewAppModal: React.FC
= ({onCreateFromTemplate, onWriteOwnApp, .
return (
= ({onCreateFromTemplate, onWriteOwnApp, .
>
-
+
Create From Template
Create Quickly Simple Prompt Apps From UI
diff --git a/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx b/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx
new file mode 100644
index 0000000000..3a7d655407
--- /dev/null
+++ b/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx
@@ -0,0 +1,219 @@
+import {GenericObject} from "@/lib/Types"
+import {getErrorMessage} from "@/lib/helpers/errorHandler"
+import {CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons"
+import {Alert, Modal, Typography, theme} from "antd"
+import {useRouter} from "next/router"
+import React, {useEffect, useState} from "react"
+import {createUseStyles} from "react-jss"
+
+const useStyles = createUseStyles({
+ statusRow: {
+ marginTop: 8,
+ display: "flex",
+ alignItems: "flex-start",
+ gap: 8,
+
+ "& .anticon": {
+ marginTop: 4,
+ },
+ },
+ warning: {
+ margin: "8px 0",
+ marginLeft: -2,
+ },
+ statusSteps: {
+ marginTop: 12,
+ },
+})
+
+interface Props {
+ loading: boolean
+ onErrorRetry?: () => void
+ onTimeoutRetry?: () => void
+ statusData: {status: string; details?: any; appId?: string}
+ appName: string
+}
+
+const CreateAppStatusModal: React.FC
> = ({
+ loading,
+ onErrorRetry,
+ onTimeoutRetry,
+ statusData,
+ appName,
+ ...props
+}) => {
+ const router = useRouter()
+ const classes = useStyles()
+ const {
+ token: {colorError, colorSuccess, colorPrimary},
+ } = theme.useToken()
+ const [messages, setMessages] = useState<{
+ [status: string]: {
+ type: "error" | "success" | "loading"
+ message: string
+ errorMessage?: string
+ }
+ }>({})
+ const [isDelayed, setIsDelayed] = useState(false)
+
+ const {appId, status, details} = statusData
+ const isError = ["bad_request", "error"].includes(status)
+ const isTimeout = status === "timeout"
+ const isSuccess = status === "success"
+ const closable = isError || isTimeout
+
+ const reset = () => {
+ setMessages({})
+ setIsDelayed(false)
+ }
+
+ const onOk = (e: any) => {
+ reset()
+ if (isError) {
+ onErrorRetry?.()
+ } else if (isTimeout) {
+ onTimeoutRetry?.()
+ } else if (isSuccess) {
+ props.onCancel?.(e)
+ if (appId) router.push(`/apps/${appId}/playground`)
+ }
+ }
+
+ useEffect(() => {
+ setMessages((prev) => {
+ let obj: GenericObject
+ switch (status) {
+ case "creating_app":
+ obj = {
+ ...prev,
+ [status]: {
+ type: "loading",
+ message: "Creating variant from template image",
+ },
+ }
+ if (obj.fetching_image?.type === "loading") obj.fetching_image.type = "success"
+ return obj
+ case "starting_app":
+ obj = {
+ ...prev,
+ [status]: {
+ type: "loading",
+ message: "Waiting for the app to start",
+ },
+ }
+ if (obj.creating_app?.type === "loading") obj.creating_app.type = "success"
+ return obj
+ case "success":
+ obj = {
+ ...prev,
+ [status]: {
+ type: "success",
+ message: "App created successfully!",
+ },
+ }
+ if (obj.starting_app?.type === "loading") obj.starting_app.type = "success"
+ if (appId) router.push(`/apps/${appId}/playground`)
+ return obj
+ case "bad_request":
+ case "error":
+ const lastStatus = Object.keys(prev).pop() ?? ""
+ return {
+ ...prev,
+ [lastStatus]: {
+ ...prev[lastStatus],
+ type: "error",
+ errorMessage: `Error: ${getErrorMessage(details)}`,
+ },
+ }
+ case "timeout":
+ return {
+ ...prev,
+ starting_app: {
+ ...prev.starting_app,
+ type: "error",
+ errorMessage: `Error: The app took too long to start. Press the "Retry" button if you want to try again.`,
+ },
+ }
+ case "cleanup":
+ return {
+ ...prev,
+ [status]: {
+ type: "loading",
+ message: "Performing cleaning up before retrying",
+ },
+ }
+ }
+ return prev
+ })
+ }, [status])
+
+ useEffect(() => {
+ if (!props.open) reset()
+ else {
+ const timeout = setTimeout(() => {
+ setIsDelayed(true)
+ }, 20000)
+ return () => clearTimeout(timeout)
+ }
+ }, [props.open])
+
+ return (
+
+ {!closable && isDelayed && (
+
+ )}
+
+ Creating your app "{appName}" . This takes around 30 seconds.
+
+
+
+ {Object.values(messages).map(({type, message, errorMessage}, ix) => (
+
+ {type === "success" ? (
+
+ ) : type === "error" ? (
+
+ ) : (
+
+ )}
+
+ {message}
+ {errorMessage && (
+ <>
+
+ {errorMessage}
+ >
+ )}
+
+
+ ))}
+
+
+ )
+}
+
+export default CreateAppStatusModal
diff --git a/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx b/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx
new file mode 100644
index 0000000000..ea634fbd98
--- /dev/null
+++ b/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx
@@ -0,0 +1,91 @@
+import {useAppTheme} from "@/components/Layout/ThemeContextProvider"
+import {AppstoreAddOutlined, CodeOutlined} from "@ant-design/icons"
+import {Col, Modal, Row, Typography} from "antd"
+import React from "react"
+import {createUseStyles} from "react-jss"
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
+
+const useStyles = createUseStyles({
+ modal: {
+ "& .ant-modal-close": {
+ top: 23,
+ },
+ },
+ title: {
+ margin: 0,
+ },
+ row: {
+ marginTop: 20,
+ },
+ col: ({themeMode}: StyleProps) => ({
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ textAlign: "center",
+ justifyContent: "center",
+ padding: "1rem",
+ cursor: "pointer",
+ border: `1px solid`,
+ borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.15)",
+ borderRadius: 8,
+ transition: "all 0.3s",
+
+ "& > .anticon": {
+ fontSize: 24,
+ },
+ "& > h5": {
+ marginTop: "0.75rem",
+ marginBottom: "0.25rem",
+ color: "inherit",
+ },
+ "& > .ant-typography-secondary": {
+ fontSize: 13,
+ },
+ "&:hover": {
+ borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.4)" : "rgba(5, 5, 5, 0.3)",
+ },
+ }),
+})
+
+const {Title, Text} = Typography
+
+type Props = React.ComponentProps & {}
+
+const MaxAppModal: React.FC = ({...props}) => {
+ const {appTheme} = useAppTheme()
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
+
+ return (
+
+ Free Limit Reached
+
+ }
+ width={600}
+ {...props}
+ >
+ You've reached the maximum number of apps for the free version.
+
+ To create more apps{" "}
+
schedule a call to get
+ full access to the platform.
+
+
+ Questions? Feel free to reach out to our support in{" "}
+
+ Slack
+
+ .
+
+
+ )
+}
+
+export default MaxAppModal
diff --git a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx
index 47f75876bf..0e9fa28692 100644
--- a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx
+++ b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx
@@ -1,14 +1,36 @@
+import Link from "next/link"
+import CopyButton from "@/components/CopyButton/CopyButton"
+import {useAppTheme} from "@/components/Layout/ThemeContextProvider"
import {Modal, Typography} from "antd"
import React, {useEffect, useRef} from "react"
import {createUseStyles} from "react-jss"
import YouTube, {YouTubeProps} from "react-youtube"
+import {isDemo} from "@/lib/helpers/utils"
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
const useStyles = createUseStyles({
- modal: {
+ modal: ({themeMode}: StyleProps) => ({
+ "& .ant-modal-content": {
+ backgroundColor: themeMode === "dark" ? "rgb(13, 17, 23)" : "#fff",
+ },
+ "& .ant-modal-header": {
+ backgroundColor: themeMode === "dark" ? "rgb(13, 17, 23)" : "#fff",
+ },
"& .ant-modal-close": {
top: 23,
},
- },
+ "& .ant-modal": {
+ width: "auto !important",
+ },
+ "& .ant-modal-body": {
+ display: "flex",
+ alignItems: "center",
+ gap: 10,
+ },
+ }),
title: {
margin: 0,
},
@@ -16,6 +38,45 @@ const useStyles = createUseStyles({
marginTop: 16,
height: 360,
},
+ wrapper: {
+ width: 450,
+ marginRight: 10,
+ "& ol": {
+ padding: "0 5px",
+ listStyleType: "none",
+ },
+ },
+ copyBtn: {
+ backgroundColor: "transparent",
+ alignSelf: "flex-start",
+ },
+ container: {
+ margin: "20px 0",
+ "& li": {
+ fontSize: 16,
+ fontWeight: 600,
+ marginBottom: 3,
+ },
+ },
+ command: ({themeMode}: StyleProps) => ({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ backgroundColor: themeMode === "light" ? "#f6f8fa" : "#161b22",
+ padding: "5px 10px",
+ borderRadius: 5,
+ border: `1px solid ${themeMode === "light" ? "#d0d7de" : "#30363d"}`,
+ color: themeMode === "light" ? "#1f2328" : "#e6edf3",
+ "& span": {
+ letterSpacing: 0.3,
+ },
+ }),
+ youtube: {
+ "& iframe": {
+ height: 430,
+ width: 560,
+ },
+ },
})
const {Title} = Typography
@@ -23,7 +84,8 @@ const {Title} = Typography
type Props = React.ComponentProps & {}
const WriteOwnAppModal: React.FC = ({...props}) => {
- const classes = useStyles()
+ const {appTheme} = useAppTheme()
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
const youtubePlayer = useRef(null)
const onPlayerReady: YouTubeProps["onStateChange"] = (event) => {
@@ -45,10 +107,9 @@ const WriteOwnAppModal: React.FC = ({...props}) => {
footer={null}
title={
- Write your own app
+ Write Your Own App
}
- width={688}
{...props}
afterClose={() => {
if (youtubePlayer.current) {
@@ -56,12 +117,94 @@ const WriteOwnAppModal: React.FC = ({...props}) => {
}
}}
>
+
+
+ {isDemo() && (
+
+
+ 0. Get an API key
+
+
+ )}
+
+
1. Install agenta
+
+ pip install -U agenta
+
+
+
+
+
+
2. Clone the example application
+
+
+ git clone https://github.com/Agenta-AI/simple_prompt && cd
+ simple_prompt
+
+
+
+
+
+
3. Set up environement variable
+
+ echo -e "OPENAI_API_KEY=sk-xxx" {">"} .env
+ .env"}
+ icon={true}
+ buttonText={""}
+ className={classes.copyBtn}
+ />
+
+
+
+
4. Setup agenta (select start from blank)
+
+ agenta init
+
+
+
+
+
5. Serve an app variant
+
+ agenta variant serve --file_name app.py
+
+
+
+
+ Check out{" "}
+
+ our tutorial for writing your first LLM app
+
+
+
+
{
youtubePlayer.current = youtube
}}
+ className={classes.youtube}
/>
)
diff --git a/agenta-web/src/components/CardGrid/CardGrid.tsx b/agenta-web/src/components/CardGrid/CardGrid.tsx
index eb94a7bdf2..06899d729b 100644
--- a/agenta-web/src/components/CardGrid/CardGrid.tsx
+++ b/agenta-web/src/components/CardGrid/CardGrid.tsx
@@ -1,7 +1,24 @@
import React, {useState} from "react"
import {Card, Button, Row, Col} from "antd"
+import {createUseStyles} from "react-jss"
+
+const useStyles = createUseStyles({
+ card: {
+ width: 300,
+ height: "250px",
+ marginBottom: "20px",
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ cardBtn: {
+ height: "200px",
+ width: "220px",
+ },
+})
const CardGrid = () => {
+ const classes = useStyles()
const [cards, setCards] = useState([1]) // initial state with one card
const addCard = () => {
@@ -20,21 +37,12 @@ const CardGrid = () => {
{" "}
{/* each card takes 1/4 of the row */}
-
+
{card === cards.length ? ( // display + in the last card
+
diff --git a/agenta-web/src/components/ChatInputs/ChatInputs.tsx b/agenta-web/src/components/ChatInputs/ChatInputs.tsx
new file mode 100644
index 0000000000..d1ccada975
--- /dev/null
+++ b/agenta-web/src/components/ChatInputs/ChatInputs.tsx
@@ -0,0 +1,162 @@
+import {ChatMessage, ChatRole} from "@/lib/Types"
+import {MinusOutlined, PlusOutlined} from "@ant-design/icons"
+import {Button, Input, Select, Space, Tooltip} from "antd"
+import React, {useEffect, useRef, useState} from "react"
+import {createUseStyles} from "react-jss"
+import {useUpdateEffect} from "usehooks-ts"
+import {v4 as uuidv4} from "uuid"
+
+const useStyles = createUseStyles({
+ root: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "1rem",
+ width: "100%",
+ },
+ row: {
+ display: "flex",
+ alignItems: "center",
+ gap: "0.5rem",
+
+ "& .ant-select": {
+ width: 110,
+ alignSelf: "flex-start",
+ },
+
+ "& textarea": {
+ marginTop: "0 !important",
+ flex: 1,
+ minWidth: 240,
+ maxWidth: 800,
+ },
+ },
+})
+
+export const getDefaultNewMessage = () => ({
+ id: uuidv4(),
+ role: ChatRole.User,
+ content: "",
+})
+
+interface Props {
+ defaultValue?: ChatMessage[]
+ value?: ChatMessage[]
+ onChange?: (value: ChatMessage[]) => void
+ maxRows?: number
+ disableAdd?: boolean
+ disableRemove?: boolean
+ disableEditRole?: boolean
+ disableEditContent?: boolean
+ readonly?: boolean
+}
+
+const ChatInputs: React.FC = ({
+ defaultValue,
+ value,
+ onChange,
+ maxRows = 12,
+ disableAdd,
+ disableRemove,
+ disableEditRole,
+ disableEditContent,
+ readonly,
+}) => {
+ const classes = useStyles()
+ const [messages, setMessages] = useState(
+ value || defaultValue || [getDefaultNewMessage()],
+ )
+ const onChangeRef = useRef(onChange)
+
+ if (readonly) {
+ disableAdd = true
+ disableRemove = true
+ disableEditRole = true
+ }
+
+ const handleRoleChange = (index: number, role: ChatRole) => {
+ const newMessages = [...messages]
+ newMessages[index].role = role
+ setMessages(newMessages)
+ }
+
+ const handleInputChange = (index: number, event: React.ChangeEvent) => {
+ const {value} = event.target
+ const newMessages = [...messages]
+ newMessages[index].content = value
+ setMessages(newMessages)
+ }
+
+ const handleDelete = (index: number) => {
+ setMessages((prev) => prev.filter((_, i) => i !== index))
+ }
+
+ const handleAdd = () => {
+ setMessages((prev) => prev.concat([getDefaultNewMessage()]))
+ }
+
+ useEffect(() => {
+ onChangeRef.current = onChange
+ }, [onChange])
+
+ useUpdateEffect(() => {
+ if (onChangeRef.current) {
+ onChangeRef.current(messages)
+ }
+ }, [messages])
+
+ useUpdateEffect(() => {
+ if (Array.isArray(value)) setMessages(value)
+ }, [JSON.stringify(value)])
+
+ return (
+
+ {messages.map((msg, ix) => (
+
+ ({
+ label: role,
+ value: ChatRole[role as keyof typeof ChatRole],
+ }))}
+ value={msg.role}
+ onChange={(newRole) => handleRoleChange(ix, newRole)}
+ />
+ handleInputChange(ix, e)}
+ readOnly={readonly}
+ />
+ {messages.length > 1 && !disableRemove && (
+
+ }
+ onClick={() => handleDelete(ix)}
+ />
+
+ )}
+
+ ))}
+
+
+ {!disableAdd && (
+
+ }
+ onClick={handleAdd}
+ size="small"
+ />
+
+ )}
+
+
+ )
+}
+
+export default ChatInputs
diff --git a/agenta-web/src/components/CopyButton/CopyButton.tsx b/agenta-web/src/components/CopyButton/CopyButton.tsx
new file mode 100644
index 0000000000..e3fcf6e509
--- /dev/null
+++ b/agenta-web/src/components/CopyButton/CopyButton.tsx
@@ -0,0 +1,43 @@
+import {copyToClipboard} from "@/lib/helpers/copyToClipboard"
+import {CheckOutlined, CopyOutlined} from "@ant-design/icons"
+import {Button, notification} from "antd"
+import React, {ComponentProps, useState} from "react"
+
+type Props = {
+ text: string
+ buttonText?: string | null
+ icon?: boolean
+}
+
+const CopyButton: React.FC> = ({
+ text,
+ buttonText = "Copy",
+ icon = false,
+ ...props
+}) => {
+ const [buttonIcon, setButtonIcon] = useState( )
+
+ return (
+ {
+ if (text === "") return
+ const copied = await copyToClipboard(text)
+ if (copied) {
+ notification.success({
+ message: "Copied to clipboard!",
+ })
+ setButtonIcon( )
+ setTimeout(() => {
+ setButtonIcon( )
+ }, 3000)
+ }
+ }}
+ >
+ {buttonText}
+
+ )
+}
+
+export default CopyButton
diff --git a/agenta-web/src/components/DraggableTabNode/DraggableTabNode.tsx b/agenta-web/src/components/DraggableTabNode/DraggableTabNode.tsx
new file mode 100644
index 0000000000..debda21a04
--- /dev/null
+++ b/agenta-web/src/components/DraggableTabNode/DraggableTabNode.tsx
@@ -0,0 +1,37 @@
+import React from "react"
+import {useSortable} from "@dnd-kit/sortable"
+import {CSS} from "@dnd-kit/utilities"
+
+// Defines the props for DraggableTabNode component.
+interface DraggableTabNodeProps extends React.HTMLAttributes {
+ "data-node-key": string // Unique identifier for the draggable item.
+}
+
+// This component wraps each tab label to make it draggable.
+const DraggableTabNode: React.FC = ({
+ "data-node-key": key,
+ children,
+ ...props
+}) => {
+ // useSortable hook provides the necessary handlers and attributes for drag-and-drop.
+ const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id: key})
+
+ // CSS styles to apply transform and transition for the dragging effect.
+ const style: React.CSSProperties = {
+ ...props.style,
+ transform: CSS.Transform.toString(transform ? {...transform, scaleX: 1} : null),
+ transition,
+ userSelect: "none",
+ cursor: "pointer", // Changes cursor to indicate draggability.
+ }
+
+ // Clones the children element to add refs and drag-and-drop related properties.
+ return React.cloneElement(children as React.ReactElement, {
+ ref: setNodeRef,
+ style,
+ ...attributes,
+ ...listeners,
+ })
+}
+
+export default DraggableTabNode
diff --git a/agenta-web/src/components/DynamicCodeBlock/CodeBlock.tsx b/agenta-web/src/components/DynamicCodeBlock/CodeBlock.tsx
index a462474243..df6ebc8002 100644
--- a/agenta-web/src/components/DynamicCodeBlock/CodeBlock.tsx
+++ b/agenta-web/src/components/DynamicCodeBlock/CodeBlock.tsx
@@ -4,18 +4,26 @@ import {Typography} from "antd"
import {CopyOutlined} from "@ant-design/icons"
import {FC} from "react"
import {useAppTheme} from "../Layout/ThemeContextProvider"
+import {createUseStyles} from "react-jss"
interface CodeBlockProps {
language: string
value: string
}
+const useStyles = createUseStyles({
+ container: {
+ margin: 0,
+ },
+})
+
const CodeBlock: FC = ({language, value}) => {
const {Paragraph} = Typography
const {appTheme} = useAppTheme()
+ const classes = useStyles()
return (
-
+
void
- onLanguageChange?: (selectedLanguage: LanguageItem) => void
}
-const DynamicCodeBlock: React.FC = ({
- codeSnippets,
- includeVariantsDropdown = false,
- variants,
- selectedVariant,
- onVariantChange,
-}) => {
+const useStyles = createUseStyles({
+ container: {
+ borderRadius: 10,
+ display: "flex",
+ flexDirection: "column",
+ },
+ header: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ },
+ headerText: {
+ fontSize: "1em",
+ marginRight: "10px",
+ },
+ copyBtn: {
+ marginLeft: "15px",
+ },
+})
+
+const DynamicCodeBlock: React.FC = ({codeSnippets}) => {
+ const classes = useStyles()
const supportedLanguages: LanguageItem[] = [
{displayName: "Python", languageKey: "python"},
{displayName: "cURL", languageKey: "bash"},
@@ -39,125 +49,36 @@ const DynamicCodeBlock: React.FC = ({
setSelectedLanguage(newSelectedLanguage)
}
- const variantsItems: MenuProps["items"] = variants
- ? variants.map((variant) => {
- return {
- label: variant.variantName,
- key: variant.variantName,
- }
- })
- : []
-
- const handleVariantClick = ({key}: {key: string}) => {
- const newSelectedVariant = variants.find((variant) => variant.variantName === key)
- if (newSelectedVariant) {
- onVariantChange?.(key)
- }
- }
- const copyToClipboard = async (e: React.MouseEvent) => {
- e.preventDefault()
- try {
- await navigator.clipboard.writeText(codeSnippets[selectedLanguage.displayName])
- } catch (err) {
- console.error("Failed to copy text to clipboard")
- }
- }
-
- const {Text, Title} = Typography
+ const {Text} = Typography
return (
-
-
-
-
- Select a variant then use this endpoint to send requests to the LLM app.
-
-
-
-
- {" "}
- {/* Larger font */}
-
- Variant:
-
- {includeVariantsDropdown && (
-
-
-
- {selectedVariant?.variantName || "Select a variant"}
-
-
-
-
- )}
-
-
-
- Language:
-
- {selectedLanguage && (
-
-
-
- {selectedLanguage.displayName}
-
-
-
-
- )}
-
- Copy
-
+
+
+
+ Language:
+
+ {selectedLanguage && (
+
+
+
+ {selectedLanguage.displayName}
+
+
+
+
+ )}
+
+
{selectedLanguage && (
diff --git a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx
index f2c452a14c..9439003f9f 100644
--- a/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx
+++ b/agenta-web/src/components/EvaluationTable/ABTestingEvaluationTable.tsx
@@ -1,35 +1,49 @@
import {useState, useEffect} from "react"
import type {ColumnType} from "antd/es/table"
import {CaretRightOutlined} from "@ant-design/icons"
-import {Button, Input, Space, Spin, Table, message} from "antd"
-import {Variant, Parameter} from "@/lib/Types"
-import {updateEvaluationScenario, callVariant} from "@/lib/services/api"
-import {useVariant} from "@/lib/hooks/useVariant"
+import {
+ Button,
+ Card,
+ Col,
+ Input,
+ Radio,
+ Row,
+ Space,
+ Spin,
+ Statistic,
+ Table,
+ Typography,
+ message,
+} from "antd"
+import {
+ updateEvaluationScenario,
+ callVariant,
+ fetchEvaluationResults,
+ updateEvaluation,
+} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
import {useRouter} from "next/router"
import {EvaluationFlow} from "@/lib/enums"
-import {fetchVariants} from "@/lib/services/api"
+import {createUseStyles} from "react-jss"
+import {exportABTestingEvaluationData} from "@/lib/helpers/evaluate"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {useQueryParam} from "@/hooks/useQuery"
+import EvaluationCardView from "../Evaluations/EvaluationCardView"
+import {Evaluation, EvaluationScenario, KeyValuePair, Variant} from "@/lib/Types"
+import {EvaluationTypeLabels, camelToSnake} from "@/lib/helpers/utils"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
interface EvaluationTableProps {
- evaluation: any
+ evaluation: Evaluation
columnsCount: number
evaluationScenarios: ABTestingEvaluationTableRow[]
}
-interface ABTestingEvaluationTableRow {
- id?: string
- inputs: {
- input_name: string
- input_value: string
- }[]
- outputs: {
- variant_name: string
- variant_output: string
- }[]
- columnData0: string
- columnData1: string
- vote: string
+export type ABTestingEvaluationTableRow = EvaluationScenario & {
evaluationFlow: EvaluationFlow
-}
+} & {[variantId: string]: string}
/**
*
* @param evaluation - Evaluation object
@@ -38,119 +52,231 @@ interface ABTestingEvaluationTableRow {
* @returns
*/
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestBtn: {
+ width: "100%",
+ display: "flex",
+ justifyContent: "flex-end",
+ "& button": {
+ marginLeft: 10,
+ },
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ card: {
+ marginBottom: 20,
+ },
+ statCorrect: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ statWrong: {
+ "& .ant-statistic-content-value": {
+ color: "#cf1322",
+ },
+ },
+ viewModeRow: {
+ display: "flex",
+ justifyContent: "flex-end",
+ margin: "1rem 0",
+ position: "sticky",
+ top: 36,
+ zIndex: 1,
+ },
+})
+
const ABTestingEvaluationTable: React.FC
= ({
evaluation,
evaluationScenarios,
- columnsCount,
}) => {
+ const classes = useStyles()
const router = useRouter()
- const appName = Array.isArray(router.query.app_name)
- ? router.query.app_name[0]
- : router.query.app_name || ""
+ const appId = router.query.app_id as string
const variants = evaluation.variants
- const variantData = variants.map((variant: Variant) => {
- const {inputParams, optParams, URIPath, isLoading, isError, error} = useVariant(
- appName,
- variant,
- )
-
- return {
- inputParams,
- optParams,
- URIPath,
- isLoading,
- isError,
- error,
- }
- })
+ const variantData = useVariants(appId, variants)
const [rows, setRows] = useState([])
+ const [evaluationStatus, setEvaluationStatus] = useState(evaluation.status)
+ const [evaluationResults, setEvaluationResults] = useState(null)
+ const [viewMode, setViewMode] = useQueryParam("viewMode", "tabular")
+
+ let num_of_rows = evaluationResults?.votes_data.nb_of_rows || 0
+ let flag_votes = evaluationResults?.votes_data.flag_votes?.number_of_votes || 0
+ let appVariant1 =
+ evaluationResults?.votes_data?.variants_votes_data?.[evaluation.variants[0]?.variantId]
+ ?.number_of_votes || 0
+ let appVariant2 =
+ evaluationResults?.votes_data?.variants_votes_data?.[evaluation.variants[1]?.variantId]
+ ?.number_of_votes || 0
useEffect(() => {
if (evaluationScenarios) {
- setRows(evaluationScenarios)
+ const obj = [...evaluationScenarios]
+ obj.forEach((item) =>
+ item.outputs.forEach((op) => (item[op.variant_id] = op.variant_output)),
+ )
+ setRows(obj)
}
}, [evaluationScenarios])
const handleInputChange = (
e: React.ChangeEvent,
- rowIndex: number,
- inputFieldKey: number,
+ id: string,
+ inputIndex: number,
) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
const newRows = [...rows]
- newRows[rowIndex].inputs[inputFieldKey].input_value = e.target.value
+ newRows[rowIndex].inputs[inputIndex].input_value = e.target.value
setRows(newRows)
}
- const handleVoteClick = (rowIndex: number, vote: string) => {
+ useEffect(() => {
+ if (evaluationStatus === EvaluationFlow.EVALUATION_FINISHED) {
+ fetchEvaluationResults(evaluation.id)
+ .then((data) => setEvaluationResults(data))
+ .catch((err) => console.error("Failed to fetch results:", err))
+ .then(() => {
+ updateEvaluation(evaluation.id, {status: EvaluationFlow.EVALUATION_FINISHED})
+ })
+ .catch((err) => console.error("Failed to fetch results:", err))
+ }
+ }, [evaluationStatus, evaluation.id])
+
+ const handleVoteClick = (id: string, vote: string) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
const evaluation_scenario_id = rows[rowIndex].id
if (evaluation_scenario_id) {
setRowValue(rowIndex, "vote", "loading")
- // TODO: improve this to make it dynamic
- const appVariantNameX = variants[0].variantName
- const appVariantNameY = variants[1].variantName
- const outputVariantX = rows[rowIndex].columnData0
- const outputVariantY = rows[rowIndex].columnData1
const data = {
vote: vote,
- outputs: [
- {variant_name: appVariantNameX, variant_output: outputVariantX},
- {variant_name: appVariantNameY, variant_output: outputVariantY},
- ],
+ outputs: variants.map((v: Variant) => ({
+ variant_id: v.variantId,
+ variant_output: rows[rowIndex][v.variantId],
+ })),
+ inputs: rows[rowIndex].inputs,
}
- updateEvaluationScenario(
- evaluation.id,
- evaluation_scenario_id,
- data,
- evaluation.evaluationType,
- )
- .then((data) => {
- setRowValue(rowIndex, "vote", vote)
- })
- .catch((err) => {
- console.error(err)
- })
+ updateEvaluationScenarioData(evaluation_scenario_id, data)
}
}
- const runAllEvaluations = async () => {
- const promises: Promise[] = []
-
- for (let i = 0; i < rows.length; i++) {
- promises.push(runEvaluation(i))
- }
+ const updateEvaluationScenarioData = async (
+ id: string,
+ data: Partial,
+ showNotification: boolean = true,
+ ) => {
+ await updateEvaluationScenario(
+ evaluation.id,
+ id,
+ Object.keys(data).reduce(
+ (acc, key) => ({
+ ...acc,
+ [camelToSnake(key)]: data[key as keyof EvaluationScenario],
+ }),
+ {},
+ ),
+ evaluation.evaluationType,
+ )
+ .then(() => {
+ Object.keys(data).forEach((key) => {
+ setRowValue(
+ evaluationScenarios.findIndex((item) => item.id === id),
+ key,
+ data[key as keyof EvaluationScenario],
+ )
+ })
+ if (showNotification) message.success("Evaluation Updated!")
+ })
+ .catch(console.error)
+ }
- Promise.all(promises)
- .then(() => console.log("All functions finished."))
+ const runAllEvaluations = async () => {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_STARTED)
+ Promise.all(rows.map((row) => runEvaluation(row.id!, rows.length - 1, false)))
+ .then(() => {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
+ message.success("Evaluations Updated!")
+ })
.catch((err) => console.error("An error occurred:", err))
}
- const runEvaluation = async (rowIndex: number) => {
+ const runEvaluation = async (
+ id: string,
+ count: number = 1,
+ showNotification: boolean = true,
+ ) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
acc[item.input_name] = item.input_value
return acc
}, {})
- const columnsDataNames = ["columnData0", "columnData1"]
- columnsDataNames.forEach(async (columnName: any, idx: number) => {
- setRowValue(rowIndex, columnName, "loading...")
- try {
- let result = await callVariant(
- inputParamsDict,
- variantData[idx].inputParams,
- variantData[idx].optParams,
- variantData[idx].URIPath,
- )
- setRowValue(rowIndex, columnName, result)
- setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
- } catch (e) {
- setRowValue(rowIndex, columnName, "")
- message.error("Oops! Something went wrong")
- }
- })
+ const outputs = rows[rowIndex].outputs.reduce(
+ (acc, op) => ({...acc, [op.variant_id]: op.variant_output}),
+ {},
+ )
+
+ await Promise.all(
+ variants.map(async (variant: Variant, idx: number) => {
+ setRowValue(rowIndex, variant.variantId, "loading...")
+ try {
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ setRowValue(rowIndex, variant.variantId, result)
+ ;(outputs as KeyValuePair)[variant.variantId] = result
+ setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
+ if (idx === variants.length - 1) {
+ if (count === 1 || count === rowIndex) {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
+ }
+ }
+ } catch (err) {
+ console.log("Error running evaluation:", err)
+ setRowValue(rowIndex, variant.variantId, "")
+ }
+ }),
+ )
+
+ updateEvaluationScenarioData(
+ id,
+ {
+ outputs: Object.keys(outputs).map((key) => ({
+ variant_id: key,
+ variant_output: outputs[key as keyof typeof outputs],
+ })),
+ inputs: rows[rowIndex].inputs,
+ },
+ showNotification,
+ )
}
const setRowValue = (
@@ -163,24 +289,16 @@ const ABTestingEvaluationTable: React.FC = ({
setRows(newRows)
}
- const dynamicColumns: ColumnType[] = Array.from(
- {length: columnsCount},
- (_, i) => {
- const columnKey = `columnData${i}`
+ const dynamicColumns: ColumnType[] = variants.map(
+ (variant: Variant) => {
+ const columnKey = variant.variantId
return {
title: (
App Variant:
-
- {variants ? variants[i].variantName : ""}
+
+ {variants ? variant.variantName : ""}
),
@@ -188,13 +306,14 @@ const ABTestingEvaluationTable: React.FC = ({
key: columnKey,
width: "20%",
render: (text: any, record: ABTestingEvaluationTableRow, rowIndex: number) => {
+ if (text) return text
if (record.outputs && record.outputs.length > 0) {
const outputValue = record.outputs.find(
- (output: any) => output.variant_name === variants[i].variantName,
+ (output: any) => output.variant_id === columnKey,
)?.variant_output
return {outputValue}
}
- return text
+ return ""
},
}
},
@@ -204,53 +323,38 @@ const ABTestingEvaluationTable: React.FC = ({
{
key: "1",
title: (
-
+
Inputs (Test set:
-
- {evaluation.testset.name}
-
+ {evaluation.testset.name}
)
-
}>
- Run All
-
),
dataIndex: "inputs",
render: (text: any, record: ABTestingEvaluationTableRow, rowIndex: number) => (
- {record &&
- record.inputs &&
- record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
- record.inputs.map((input: any, index: number) => (
-
- handleInputChange(e, rowIndex, index)}
- />
-
- ))}
+ {evaluation.testset.testsetChatColumn
+ ? evaluation.testset.csvdata[rowIndex][
+ evaluation.testset.testsetChatColumn
+ ] || " - "
+ : record &&
+ record.inputs &&
+ record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
+ record.inputs.map((input: any, index: number) => (
+
+ handleInputChange(e, record.id, index)}
+ />
+
+ ))}
-
+
runEvaluation(rowIndex)}
+ onClick={() => runEvaluation(record.id!)}
icon={ }
- style={{marginLeft: 10}}
>
Run
@@ -269,30 +373,33 @@ const ABTestingEvaluationTable: React.FC
= ({
handleVoteClick(rowIndex, variants[0].variantName)}
+ onClick={() => handleVoteClick(record.id, variants[0].variantId)}
>
{`Variant: ${variants[0].variantName}`}
handleVoteClick(rowIndex, variants[1].variantName)}
+ onClick={() => handleVoteClick(record.id, variants[1].variantId)}
>
{`Variant: ${variants[1].variantName}`}
= ({
: true
}
danger
- onClick={() => handleVoteClick(rowIndex, "0")}
+ onClick={() => handleVoteClick(record.id, "0")}
>
Both are bad
@@ -313,13 +420,93 @@ const ABTestingEvaluationTable: React.FC = ({
return (
-
"editable-row"}
- rowKey={(record) => record.id}
- />
+ {EvaluationTypeLabels.human_a_b_testing}
+
+
+
+
+
+ Run All
+
+ exportABTestingEvaluationData(evaluation, rows)}
+ disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED}
+ >
+ Export results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setViewMode(e.target.value)}
+ value={viewMode}
+ optionType="button"
+ />
+
+
+ {viewMode === "tabular" ? (
+ "editable-row"}
+ rowKey={(record) => record.id!}
+ />
+ ) : (
+ handleVoteClick(id, vote as string)}
+ onInputChange={handleInputChange}
+ updateEvaluationScenarioData={updateEvaluationScenarioData}
+ evaluation={evaluation}
+ />
+ )}
)
}
diff --git a/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx
index 40d663a5d2..8917212f2a 100644
--- a/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx
+++ b/agenta-web/src/components/EvaluationTable/AICritiqueEvaluationTable.tsx
@@ -1,6 +1,6 @@
import {useState, useEffect} from "react"
import type {ColumnType} from "antd/es/table"
-import {BarChartOutlined, LineChartOutlined} from "@ant-design/icons"
+import {LineChartOutlined} from "@ant-design/icons"
import {
Button,
Card,
@@ -15,18 +15,25 @@ import {
Typography,
message,
} from "antd"
-import {Evaluation, Variant} from "@/lib/Types"
+import {Evaluation} from "@/lib/Types"
import {
updateEvaluationScenario,
callVariant,
fetchEvaluationResults,
updateEvaluation,
+ evaluateAICritiqueForEvalScenario,
} from "@/lib/services/api"
-import {useVariant} from "@/lib/hooks/useVariant"
+import {useVariants} from "@/lib/hooks/useVariant"
import {useRouter} from "next/router"
-import {EvaluationFlow} from "@/lib/enums"
-import TextArea from "antd/es/input/TextArea"
+import {EvaluationFlow, EvaluationType} from "@/lib/enums"
import {getOpenAIKey} from "@/lib/helpers/utils"
+import {createUseStyles} from "react-jss"
+import {exportAICritiqueEvaluationData} from "@/lib/helpers/evaluate"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {useAppTheme} from "../Layout/ThemeContextProvider"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
interface AICritiqueEvaluationTableProps {
evaluation: Evaluation
@@ -41,14 +48,18 @@ interface AICritiqueEvaluationTableRow {
input_value: string
}[]
outputs: {
- variant_name: string
+ variant_id: string
variant_output: string
}[]
columnData0: string
correctAnswer: string
- evaluation: string
+ score: string
evaluationFlow: EvaluationFlow
}
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
/**
*
* @param evaluation - Evaluation object
@@ -57,37 +68,98 @@ interface AICritiqueEvaluationTableRow {
* @returns
*/
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: ({themeMode}: StyleProps) => ({
+ marginTop: 16,
+ width: "100%",
+ border: "1px solid #ccc",
+ marginRight: "24px",
+ marginBottom: 30,
+ background: themeMode === "light" ? "rgb(246 253 245)" : "#000000",
+ "& .ant-card-head": {
+ minHeight: 44,
+ padding: "0px 12px",
+ },
+ "& .ant-card-body": {
+ padding: "4px 16px",
+ border: "0px solid #ccc",
+ },
+ }),
+ cardTextarea: {
+ height: 120,
+ padding: "0px 0px",
+ },
+ row: {marginBottom: 20},
+ evaluationResult: ({themeMode}: StyleProps) => ({
+ padding: "30px 10px",
+ marginBottom: 20,
+ border: "1px solid #ccc",
+ background: themeMode === "light" ? "rgb(244 244 244)" : "#000000",
+ color: themeMode === "light" ? "#000" : "#fff",
+ borderRadius: 5,
+ }),
+ h3: {
+ marginTop: 0,
+ },
+ resultDataRow: {
+ maxWidth: "100%",
+ overflowX: "auto",
+ whiteSpace: "nowrap",
+ },
+ resultDataCol: {
+ display: "inline-block",
+ },
+ resultDataCard: {
+ width: 200,
+ margin: "0 4px",
+ },
+ stat: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+})
+
const AICritiqueEvaluationTable: React.FC = ({
evaluation,
evaluationScenarios,
columnsCount,
}) => {
+ const {appTheme} = useAppTheme()
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
const router = useRouter()
- const appName = Array.isArray(router.query.app_name)
- ? router.query.app_name[0]
- : router.query.app_name || ""
+ const appId = router.query.app_id as string
const variants = evaluation.variants
- const variantData = variants.map((variant: Variant) => {
- const {inputParams, optParams, URIPath, isLoading, isError, error} = useVariant(
- appName,
- variant,
- )
-
- return {
- inputParams,
- optParams,
- URIPath,
- isLoading,
- isError,
- error,
- }
- })
+ const variantData = useVariants(appId, variants)
const [rows, setRows] = useState([])
- const [evaluationPromptTemplate, setEvaluationPromptTemplate] =
- useState(`We have an LLM App that we want to evaluate its outputs.
+ const [evaluationPromptTemplate, setEvaluationPromptTemplate] = useState(
+ evaluation.evaluationTypeSettings.evaluationPromptTemplate ||
+ `We have an LLM App that we want to evaluate its outputs.
Based on the prompt and the parameters provided below evaluate the output based on the evaluation strategy below:
Evaluation strategy: 0 to 10 0 is very bad and 10 is very good.
@@ -98,7 +170,8 @@ Correct Answer:{correct_answer}
Evaluate this: {app_variant_output}
Answer ONLY with one of the given grading or evaluation options.
-`)
+`,
+ )
const [shouldFetchResults, setShouldFetchResults] = useState(false)
const [evaluationStatus, setEvaluationStatus] = useState(evaluation.status)
@@ -125,12 +198,17 @@ Answer ONLY with one of the given grading or evaluation options.
}, [evaluationScenarios])
useEffect(() => {
- if (evaluationStatus === EvaluationFlow.EVALUATION_FINISHED) {
+ if (evaluationStatus === EvaluationFlow.EVALUATION_FINISHED && shouldFetchResults) {
fetchEvaluationResults(evaluation.id)
.then((data) => setEvaluationResults(data))
.catch((err) => console.error("Failed to fetch results:", err))
.then(() => {
- updateEvaluation(evaluation.id, {status: EvaluationFlow.EVALUATION_FINISHED})
+ updateEvaluation(evaluation.id, {
+ status: EvaluationFlow.EVALUATION_FINISHED,
+ evaluation_type_settings: {
+ evaluation_prompt_template: evaluationPromptTemplate,
+ },
+ })
})
.catch((err) => console.error("Failed to fetch results:", err))
}
@@ -138,7 +216,7 @@ Answer ONLY with one of the given grading or evaluation options.
const handleInputChange = (
e: React.ChangeEvent,
- rowIndex: number,
+ rowIndex: any,
inputFieldKey: number,
) => {
const newRows = [...rows]
@@ -154,6 +232,7 @@ Answer ONLY with one of the given grading or evaluation options.
console.log("All evaluations finished.")
} catch (err) {
console.error("An error occurred:", err)
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FAILED)
}
}
@@ -164,47 +243,58 @@ Answer ONLY with one of the given grading or evaluation options.
}, {})
const columnsDataNames = ["columnData0"]
- for (const [idx, columnName] of columnsDataNames.entries()) {
- try {
- setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
+ let idx = 0
+ for (const columnName of columnsDataNames) {
+ setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
- let result = await callVariant(
- inputParamsDict,
- variantData[idx].inputParams,
- variantData[idx].optParams,
- variantData[idx].URIPath,
- )
- setRowValue(rowIndex, columnName, result)
- await evaluate(rowIndex)
- setShouldFetchResults(true)
- } catch (e) {
- message.error("Oops! Something went wrong")
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ setRowValue(rowIndex, columnName as any, result)
+ await evaluate(rowIndex)
+ setShouldFetchResults(true)
+ if (rowIndex === rows.length - 1) {
+ message.success("Evaluation Results Saved")
}
+ idx++
}
}
const evaluate = async (rowNumber: number) => {
const evaluation_scenario_id = rows[rowNumber].id
- const appVariantNameX = variants[0].variantName
const outputVariantX = rows[rowNumber].columnData0
if (evaluation_scenario_id) {
const data = {
- outputs: [{variant_name: appVariantNameX, variant_output: outputVariantX}],
+ outputs: [{variant_id: variants[0].variantId, variant_output: outputVariantX}],
+ }
+
+ const aiCritiqueScoreResponse = await evaluateAICritiqueForEvalScenario({
+ correct_answer: rows[rowNumber].correctAnswer,
+ llm_app_prompt_template: evaluation.llmAppPromptTemplate,
inputs: rows[rowNumber].inputs,
+ outputs: data.outputs,
evaluation_prompt_template: evaluationPromptTemplate,
open_ai_key: getOpenAIKey(),
- }
+ })
try {
const responseData = await updateEvaluationScenario(
evaluation.id,
evaluation_scenario_id,
- data,
- evaluation.evaluationType,
+ {...data, score: aiCritiqueScoreResponse.data},
+ evaluation.evaluationType as EvaluationType,
)
setRowValue(rowNumber, "evaluationFlow", EvaluationFlow.EVALUATION_FINISHED)
- setRowValue(rowNumber, "evaluation", responseData.evaluation)
+ setRowValue(rowNumber, "score", aiCritiqueScoreResponse.data)
} catch (err) {
console.error(err)
}
@@ -230,14 +320,7 @@ Answer ONLY with one of the given grading or evaluation options.
title: (
App Variant:
-
+
{variants ? variants[i].variantName : ""}
@@ -246,16 +329,25 @@ Answer ONLY with one of the given grading or evaluation options.
key: columnKey,
width: "30%",
render: (text: any, record: AICritiqueEvaluationTableRow, rowIndex: number) => {
- if (record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED) {
+ if (
+ record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED &&
+ evaluationStatus === EvaluationFlow.EVALUATION_STARTED
+ ) {
return (
)
}
+ if (
+ record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED &&
+ evaluationStatus === EvaluationFlow.EVALUATION_FAILED
+ ) {
+ return
+ }
if (record.outputs && record.outputs.length > 0) {
const outputValue = record.outputs.find(
- (output: any) => output.variant_name === variants[i].variantName,
+ (output: any) => output.variant_id === variants[i].variantId,
)?.variant_output
return {outputValue}
}
@@ -270,19 +362,10 @@ Answer ONLY with one of the given grading or evaluation options.
key: "1",
width: "30%",
title: (
-
+
Inputs (Test set:
-
- {evaluation.testset.name}
-
+ {evaluation.testset.name}
)
@@ -290,18 +373,22 @@ Answer ONLY with one of the given grading or evaluation options.
dataIndex: "inputs",
render: (text: any, record: AICritiqueEvaluationTableRow, rowIndex: number) => (
),
},
@@ -317,22 +404,31 @@ Answer ONLY with one of the given grading or evaluation options.
{
title: "Evaluation",
dataIndex: "evaluation",
- key: "evaluation",
+ key: "score",
width: 200,
align: "center" as "left" | "right" | "center",
- render: (text: any, record: any, rowIndex: number) => {
- if (record.evaluationFlow === "COMPARISON_RUN_STARTED") {
+ render: (score: string, record: any) => {
+ if (
+ record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED &&
+ evaluationStatus === EvaluationFlow.EVALUATION_STARTED
+ ) {
return
}
+ if (
+ record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED &&
+ evaluationStatus === EvaluationFlow.EVALUATION_FAILED
+ ) {
+ return
+ }
let tagColor = ""
return (
-
+
- {rows[rowIndex].evaluation !== "" && (
-
- {record.evaluation}
+ {score !== "" && (
+
+ {record.score}
)}
@@ -349,25 +445,13 @@ Answer ONLY with one of the given grading or evaluation options.
return (
-
AI Critique Evaluation
+
AI Critique Evaluation
-
-
-
+
- }
- size="large"
- >
- Run Evaluation
-
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+ exportAICritiqueEvaluationData(evaluation, rows)}
+ disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED}
+ >
+ Export results
+
+
-
+
+ {evaluationStatus === EvaluationFlow.EVALUATION_FAILED && (
+ Failed to run evaluation
+ )}
+
{evaluationStatus === EvaluationFlow.EVALUATION_INITIALIZED && (
Run evaluation to see results!
)}
+
{evaluationStatus === EvaluationFlow.EVALUATION_STARTED && }
- {evaluationResults && evaluationResults.results_data && (
-
-
Results Data:
-
- {Object.entries(evaluationResults.results_data).map(
- ([key, value], index) => {
- return (
-
+
+ {evaluationStatus === EvaluationFlow.EVALUATION_FINISHED &&
+ evaluationResults &&
+ evaluationResults.results_data && (
+
+
Results Data:
+
+ {Object.entries(evaluationResults.results_data).map(
+ ([key, value], index) => (
+
- )
- },
- )}
-
-
- )}
+ ),
+ )}
+
+
+ )}
diff --git a/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx
new file mode 100644
index 0000000000..a085a63133
--- /dev/null
+++ b/agenta-web/src/components/EvaluationTable/CustomCodeRunEvaluationTable.tsx
@@ -0,0 +1,547 @@
+import {useState, useEffect} from "react"
+import type {ColumnType} from "antd/es/table"
+import {CodeOutlined, LineChartOutlined} from "@ant-design/icons"
+import {
+ Button,
+ Card,
+ Col,
+ Input,
+ Modal,
+ Row,
+ Space,
+ Spin,
+ Statistic,
+ Table,
+ Typography,
+ message,
+} from "antd"
+import {CustomEvaluation, Evaluation} from "@/lib/Types"
+import {
+ updateEvaluationScenario,
+ callVariant,
+ fetchEvaluationResults,
+ updateEvaluation,
+ executeCustomEvaluationCode,
+ loadTestset,
+ updateEvaluationScenarioScore,
+ fetchEvaluationScenarioResults,
+ fetchCustomEvaluationDetail,
+} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
+import {useRouter} from "next/router"
+import {EvaluationFlow, EvaluationType} from "@/lib/enums"
+import {getOpenAIKey} from "@/lib/helpers/utils"
+import {createUseStyles} from "react-jss"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {exportCustomCodeEvaluationData} from "@/lib/helpers/evaluate"
+import CodeBlock from "../DynamicCodeBlock/CodeBlock"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
+
+interface CustomCodeEvaluationTableProps {
+ evaluation: Evaluation
+ columnsCount: number
+ customEvaluationId: string
+ evaluationScenarios: CustomCodeEvaluationTableRow[]
+}
+
+interface CustomCodeEvaluationTableRow {
+ id?: string
+ inputs: {
+ input_name: string
+ input_value: string
+ }[]
+ outputs: {
+ variant_id: string
+ variant_output: string
+ }[]
+ columnData0: string
+ correctAnswer: string
+ evaluation: string
+ codeResult: string
+ evaluationFlow: EvaluationFlow
+}
+
+interface IVariantInputs {
+ input_name: string
+ input_value: string
+}
+
+interface IScenarioScore {
+ scenario_id: string
+ score: string
+}
+
+/**
+ *
+ * @param evaluation - Evaluation object
+ * @param evaluationScenarios - Evaluation rows
+ * @param columnsCount - Number of variants to compare face to face (per default 2)
+ * @returns
+ */
+
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: {
+ marginBottom: 20,
+ },
+ codeButton: {
+ marginBottom: 20,
+ },
+ cardTextarea: {
+ height: 120,
+ padding: "0px 0px",
+ },
+ row: {marginBottom: 20},
+ evaluationResult: {
+ padding: "30px 10px",
+ marginBottom: 20,
+ backgroundColor: "rgb(244 244 244)",
+ border: "1px solid #ccc",
+ borderRadius: 5,
+ },
+ h3: {
+ marginTop: 0,
+ },
+ resultDataRow: {
+ maxWidth: "100%",
+ overflowX: "auto",
+ whiteSpace: "nowrap",
+ },
+ resultDataCol: {
+ display: "inline-block",
+ },
+ resultDataCard: {
+ width: 200,
+ margin: "0 4px",
+ },
+ stat: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ codeBlockContainer: {
+ marginTop: 24,
+ },
+})
+
+const CustomCodeRunEvaluationTable: React.FC
= ({
+ evaluation,
+ evaluationScenarios,
+ columnsCount,
+ customEvaluationId,
+}) => {
+ const classes = useStyles()
+ const router = useRouter()
+ const appId = router.query.app_id as string
+
+ const variants = evaluation.variants
+
+ const variantData = useVariants(appId, variants)
+
+ const [rows, setRows] = useState([])
+
+ const [shouldFetchResults, setShouldFetchResults] = useState(false)
+ const [evaluationStatus, setEvaluationStatus] = useState(evaluation.status)
+ const [evaluationResults, setEvaluationResults] = useState(null)
+ const [evaluationTestsets, setEvaluationTestsets] = useState([])
+ const [listScenariosResult, setListScenariosResult] = useState([])
+ const [customEvaluation, setCustomEvaluation] = useState()
+ const [modalOpen, setModalOpen] = useState(false)
+
+ useEffect(() => {
+ if (customEvaluationId && customEvaluation?.id !== customEvaluationId) {
+ fetchCustomEvaluationDetail(customEvaluationId)
+ .then(setCustomEvaluation)
+ .catch(console.error)
+ }
+ }, [customEvaluationId])
+
+ useEffect(() => {
+ if (evaluationScenarios) {
+ setRows(evaluationScenarios)
+ Promise.all(evaluationScenarios.map((item) => retrieveScenarioScore(item.id!)))
+ }
+ }, [evaluationScenarios])
+
+ useEffect(() => {
+ if (evaluationStatus === EvaluationFlow.EVALUATION_FINISHED) {
+ fetchEvaluationResults(evaluation.id)
+ .then((data) => setEvaluationResults(data))
+ .catch((err) => console.error("Failed to fetch results:", err))
+ .then(() => {
+ updateEvaluation(evaluation.id, {status: EvaluationFlow.EVALUATION_FINISHED})
+ })
+ .catch((err) => console.error("Failed to fetch results:", err))
+ }
+ }, [evaluationStatus, evaluation.id])
+
+ useEffect(() => {
+ const getTests = async () => {
+ const data = await loadTestset(evaluation.testset._id)
+ if (data.csvdata.length > 0) {
+ setEvaluationTestsets(data.csvdata)
+ }
+ }
+
+ getTests()
+ }, [evaluation])
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ rowIndex: any,
+ inputFieldKey: number,
+ ) => {
+ const newRows = [...rows]
+ newRows[rowIndex].inputs[inputFieldKey].input_value = e.target.value
+ setRows(newRows)
+ }
+
+ const runAllEvaluations = async () => {
+ try {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_STARTED)
+ await Promise.all(rows.map((_, rowIndex) => runEvaluation(rowIndex)))
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
+ console.log("All evaluations finished.")
+ } catch (err) {
+ console.error("An error occurred:", err)
+ }
+ }
+
+ const runEvaluation = async (rowIndex: number) => {
+ const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
+ acc[item.input_name] = item.input_value
+ return acc
+ }, {})
+
+ const columnsDataNames = ["columnData0"]
+ let idx = 0
+ for (const columnName of columnsDataNames) {
+ setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
+
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ setRowValue(rowIndex, columnName as any, result)
+ await evaluate(rowIndex)
+ setShouldFetchResults(true)
+ if (rowIndex === rows.length - 1) {
+ message.success("Evaluation Results Saved")
+ }
+ idx++
+ }
+ }
+
+ const correctAnswer = (variantInputs: Array) => {
+ const {input_name, input_value} = variantInputs[0]
+ const filteredData: any = evaluationTestsets.filter(
+ (item) => item[input_name] === input_value,
+ )[0]
+ return filteredData?.correct_answer
+ }
+
+ const calcScenarioScore = (ix: number) => {
+ const item = rows[ix]
+
+ let score = +item.codeResult
+ if (!item.codeResult && item.outputs.length && listScenariosResult.length) {
+ score = +(listScenariosResult.find((res) => res.scenario_id === item.id)?.score || 0)
+ }
+ if (isNaN(score)) score = 0
+
+ return score.toFixed(2)
+ }
+
+ const retrieveScenarioScore = async (scenario_id: string) => {
+ const response: any = await fetchEvaluationScenarioResults(scenario_id)
+ setListScenariosResult((prev) => [...prev, response.data as IScenarioScore])
+ }
+
+ const evaluate = async (rowNumber: number) => {
+ const evaluation_scenario_id = rows[rowNumber].id
+ const outputVariantX = rows[rowNumber].columnData0
+
+ if (evaluation_scenario_id) {
+ const data = {
+ outputs: [{variant_id: variants[0].variantId, variant_output: outputVariantX}],
+ inputs: rows[rowNumber].inputs,
+ correct_answer: correctAnswer(rows[rowNumber].inputs),
+ open_ai_key: getOpenAIKey(),
+ }
+
+ try {
+ // Update evaluation scenario
+ const responseData = await updateEvaluationScenario(
+ evaluation.id,
+ evaluation_scenario_id,
+ data,
+ evaluation.evaluationType as EvaluationType,
+ )
+
+ // Call custom code evaluation
+ const result = await callCustomCodeHandler(
+ variants[0].variantId,
+ data.inputs,
+ data.outputs,
+ )
+ if (result) {
+ // Update the evaluation scenario with the score
+ await updateEvaluationScenarioScore(evaluation_scenario_id, result)
+ }
+ setRowValue(rowNumber, "codeResult", result)
+
+ setRowValue(rowNumber, "evaluationFlow", EvaluationFlow.EVALUATION_FINISHED)
+ setRowValue(rowNumber, "evaluation", responseData.evaluation)
+ } catch (err) {
+ console.log(err)
+ }
+ }
+ }
+
+ const callCustomCodeHandler = async (
+ variantId: string,
+ inputs: Array,
+ outputs: Array,
+ ) => {
+ const expectedTarget = correctAnswer(inputs)
+ const data = {
+ evaluation_id: customEvaluationId,
+ inputs,
+ outputs,
+ correct_answer: expectedTarget,
+ variant_id: variantId,
+ app_id: appId,
+ }
+ const response = await executeCustomEvaluationCode(data)
+ if (response.status === 200) {
+ return response.data
+ }
+ }
+
+ const setRowValue = (
+ rowIndex: number,
+ columnKey: keyof CustomCodeEvaluationTableRow,
+ value: any,
+ ) => {
+ const newRows = [...rows]
+ newRows[rowIndex][columnKey] = value as never
+ setRows(newRows)
+ }
+
+ const dynamicColumns: ColumnType[] = Array.from(
+ {length: columnsCount},
+ (_, i) => {
+ const columnKey = `columnData${i}`
+
+ return {
+ title: (
+
+ App Variant:
+
+ {variants ? variants[i].variantName : ""}
+
+
+ ),
+ dataIndex: columnKey,
+ key: columnKey,
+ width: "30%",
+ render: (text: any, record: CustomCodeEvaluationTableRow, rowIndex: number) => {
+ if (record.evaluationFlow === EvaluationFlow.COMPARISON_RUN_STARTED) {
+ return (
+
+
+
+ )
+ }
+ if (record.outputs && record.outputs.length > 0) {
+ const outputValue = record.outputs.find(
+ (output: any) => output.variant_id === variants[i].variantId,
+ )?.variant_output
+ return {outputValue}
+ }
+ return text
+ },
+ }
+ },
+ )
+
+ const columns = [
+ {
+ key: "1",
+ width: "30%",
+ title: (
+
+
+ Inputs (Test set:
+ {evaluation.testset.name}
+ )
+
+
+ ),
+ dataIndex: "inputs",
+ render: (text: any, record: CustomCodeEvaluationTableRow, rowIndex: number) => (
+
+ {evaluation.testset.testsetChatColumn
+ ? evaluation.testset.csvdata[rowIndex][
+ evaluation.testset.testsetChatColumn
+ ] || " - "
+ : record &&
+ record.inputs &&
+ record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
+ record.inputs.map((input: any, index: number) => (
+
+ handleInputChange(e, record.id, index)}
+ />
+
+ ))}
+
+ ),
+ },
+ ...dynamicColumns,
+ {
+ title: "Correct Answer",
+ dataIndex: "correctAnswer",
+ key: "correctAnswer",
+ width: "30%",
+
+ render: (text: any, record: any, rowIndex: number) => {
+ return {correctAnswer(record.inputs)}
+ },
+ },
+ {
+ title: "Result",
+ dataIndex: "codeResult",
+ key: "code_result",
+ width: 200,
+ align: "center" as "left" | "right" | "center",
+ render: (_: number, record: any, ix: number) => {
+ return (
+
+ {calcScenarioScore(ix)}
+
+ )
+ },
+ },
+ ]
+
+ return (
+
+
Custom Code Evaluation
+
+
+
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+
+ exportCustomCodeEvaluationData(
+ evaluation,
+ rows.map((item, ix) => ({
+ ...item,
+ score: calcScenarioScore(ix),
+ })),
+ )
+ }
+ disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED}
+ >
+ Export results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {customEvaluation?.python_code && (
+
}
+ className={classes.codeButton}
+ onClick={() => setModalOpen(true)}
+ >
+ Show Python Code
+
+ )}
+
+
+
"editable-row"}
+ />
+
+
+ setModalOpen(false)}
+ width={700}
+ >
+
+
+
+
+
+ )
+}
+
+export default CustomCodeRunEvaluationTable
diff --git a/agenta-web/src/components/EvaluationTable/EvaluationTableWithChat.tsx b/agenta-web/src/components/EvaluationTable/EvaluationTableWithChat.tsx
index 9e28e4e5c4..93f82467a6 100644
--- a/agenta-web/src/components/EvaluationTable/EvaluationTableWithChat.tsx
+++ b/agenta-web/src/components/EvaluationTable/EvaluationTableWithChat.tsx
@@ -3,6 +3,7 @@ import {Button, Dropdown, Input, Menu, Space, Table, Typography} from "antd"
import {AppVariant} from "@/lib/Types"
import type {ColumnType} from "antd/es/table"
import {DislikeOutlined, DownOutlined, LikeOutlined} from "@ant-design/icons"
+import {createUseStyles} from "react-jss"
interface EvaluationTableWithChatProps {
columnsCount: number
@@ -14,10 +15,23 @@ interface TableDataType {
[key: string]: any
}
+const useStyles = createUseStyles({
+ table: {
+ display: "flex",
+ justifyContent: "center",
+ marginBottom: 15,
+ },
+ title: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+})
+
const EvaluationTableWithChat: React.FC = ({
columnsCount,
appVariants,
}) => {
+ const classes = useStyles()
const [dataSource, setDataSource] = useState([])
const [selectedItems, setSelectedItems] = useState(
Array(columnsCount).fill("Select a variant"),
@@ -80,7 +94,7 @@ const EvaluationTableWithChat: React.FC = ({
return {
title: (
-
+
App Variant:
= ({
columns={columns}
footer={() => (
-
+
}>
Good
diff --git a/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx
index 13b230b5e8..eba24d3db9 100644
--- a/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx
+++ b/agenta-web/src/components/EvaluationTable/ExactMatchEvaluationTable.tsx
@@ -15,15 +15,26 @@ import {
Typography,
message,
} from "antd"
-import {Variant} from "@/lib/Types"
-import {updateEvaluationScenario, callVariant} from "@/lib/services/api"
-import {useVariant} from "@/lib/hooks/useVariant"
+import {
+ updateEvaluationScenario,
+ callVariant,
+ fetchEvaluationResults,
+ updateEvaluation,
+} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
import {useRouter} from "next/router"
import {EvaluationFlow} from "@/lib/enums"
import {evaluateWithExactMatch} from "@/lib/services/evaluations"
+import {createUseStyles} from "react-jss"
+import {exportExactEvaluationData} from "@/lib/helpers/evaluate"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+import {Evaluation} from "@/lib/Types"
+
+const {Title} = Typography
interface ExactMatchEvaluationTableProps {
- evaluation: any
+ evaluation: Evaluation
columnsCount: number
evaluationScenarios: ExactMatchEvaluationTableRow[]
}
@@ -35,7 +46,7 @@ interface ExactMatchEvaluationTableRow {
input_value: string
}[]
outputs: {
- variant_name: string
+ variant_id: string
variant_output: string
}[]
columnData0: string
@@ -51,39 +62,61 @@ interface ExactMatchEvaluationTableRow {
* @returns
*/
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: {
+ marginBottom: 20,
+ },
+ statCorrect: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ statWrong: {
+ "& .ant-statistic-content-value": {
+ color: "#cf1322",
+ },
+ },
+})
+
const ExactMatchEvaluationTable: React.FC = ({
evaluation,
evaluationScenarios,
columnsCount,
}) => {
+ const classes = useStyles()
const router = useRouter()
- const appName = Array.isArray(router.query.app_name)
- ? router.query.app_name[0]
- : router.query.app_name || ""
+ const appId = router.query.app_id as string
const variants = evaluation.variants
- const variantData = variants.map((variant: Variant) => {
- const {inputParams, optParams, URIPath, isLoading, isError, error} = useVariant(
- appName,
- variant,
- )
-
- return {
- inputParams,
- optParams,
- URIPath,
- isLoading,
- isError,
- error,
- }
- })
+ const variantData = useVariants(appId, variants)
const [rows, setRows] = useState([])
const [wrongAnswers, setWrongAnswers] = useState(0)
const [correctAnswers, setCorrectAnswers] = useState(0)
const [accuracy, setAccuracy] = useState(0)
-
- const {Title} = Typography
+ const [evaluationStatus, setEvaluationStatus] = useState(evaluation.status)
useEffect(() => {
if (evaluationScenarios) {
@@ -91,6 +124,16 @@ const ExactMatchEvaluationTable: React.FC = ({
}
}, [evaluationScenarios])
+ useEffect(() => {
+ if (evaluationStatus === EvaluationFlow.EVALUATION_FINISHED) {
+ fetchEvaluationResults(evaluation.id)
+ .then(() => {
+ updateEvaluation(evaluation.id, {status: EvaluationFlow.EVALUATION_FINISHED})
+ })
+ .catch((err) => console.error("Failed to fetch results:", err))
+ }
+ }, [evaluationStatus, evaluation.id])
+
useEffect(() => {
if (correctAnswers + wrongAnswers > 0) {
setAccuracy((correctAnswers / (correctAnswers + wrongAnswers)) * 100)
@@ -111,7 +154,7 @@ const ExactMatchEvaluationTable: React.FC = ({
const handleInputChange = (
e: React.ChangeEvent,
- rowIndex: number,
+ rowIndex: any,
inputFieldKey: number,
) => {
const newRows = [...rows]
@@ -120,6 +163,7 @@ const ExactMatchEvaluationTable: React.FC = ({
}
const runAllEvaluations = async () => {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_STARTED)
const promises: Promise[] = []
for (let i = 0; i < rows.length; i++) {
@@ -129,6 +173,7 @@ const ExactMatchEvaluationTable: React.FC = ({
Promise.all(promises)
.then(() => {
console.log("All functions finished.")
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
})
.catch((err) => console.error("An error occurred:", err))
}
@@ -145,16 +190,24 @@ const ExactMatchEvaluationTable: React.FC = ({
try {
let result = await callVariant(
inputParamsDict,
- variantData[idx].inputParams,
- variantData[idx].optParams,
- variantData[idx].URIPath,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
)
+
setRowValue(rowIndex, columnName, result)
setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
evaluate(rowIndex)
- } catch (e) {
+ if (rowIndex === rows.length - 1) {
+ message.success("Evaluation Results Saved")
+ }
+ } catch (err) {
+ console.log("Error running evaluation:", err)
setRowValue(rowIndex, columnName, "")
- message.error("Oops! Something went wrong")
}
})
}
@@ -176,13 +229,12 @@ const ExactMatchEvaluationTable: React.FC = ({
const evaluation_scenario_id = rows[rowNumber].id
// TODO: we need to improve this and make it dynamic
- const appVariantNameX = variants[0].variantName
const outputVariantX = rows[rowNumber].columnData0
if (evaluation_scenario_id) {
const data = {
score: isCorrect ? "correct" : "wrong",
- outputs: [{variant_name: appVariantNameX, variant_output: outputVariantX}],
+ outputs: [{variant_id: variants[0].variantId, variant_output: outputVariantX}],
}
updateEvaluationScenario(
@@ -191,7 +243,7 @@ const ExactMatchEvaluationTable: React.FC = ({
data,
evaluation.evaluationType,
)
- .then((data) => {
+ .then(() => {
setRowValue(rowNumber, "score", data.score)
if (isCorrect) {
setCorrectAnswers((prevCorrect) => prevCorrect + 1)
@@ -224,14 +276,7 @@ const ExactMatchEvaluationTable: React.FC = ({
title: (
App Variant:
-
+
{variants ? variants[i].variantName : ""}
@@ -242,7 +287,7 @@ const ExactMatchEvaluationTable: React.FC = ({
render: (text: any, record: ExactMatchEvaluationTableRow, rowIndex: number) => {
if (record.outputs && record.outputs.length > 0) {
const outputValue = record.outputs.find(
- (output: any) => output.variant_name === variants[i].variantName,
+ (output: any) => output.variant_id === variants[i].variantId,
)?.variant_output
return {outputValue}
}
@@ -257,19 +302,10 @@ const ExactMatchEvaluationTable: React.FC = ({
key: "1",
width: "30%",
title: (
-
+
Inputs (Test set:
-
- {evaluation.testset.name}
-
+ {evaluation.testset.name}
)
@@ -277,18 +313,22 @@ const ExactMatchEvaluationTable: React.FC
= ({
dataIndex: "inputs",
render: (text: any, record: ExactMatchEvaluationTableRow, rowIndex: number) => (
),
},
@@ -319,7 +359,11 @@ const ExactMatchEvaluationTable: React.FC = ({
{rows[rowIndex].score !== "" && (
-
+
{record.score}
)}
@@ -333,35 +377,44 @@ const ExactMatchEvaluationTable: React.FC = ({
return (
-
Exact match Evaluation
+
Exact match Evaluation
- }
- size="large"
- >
- Run Evaluation
-
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+ exportExactEvaluationData(evaluation, rows)}
+ disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED}
+ >
+ Export results
+
+
-
+
@@ -369,7 +422,6 @@ const ExactMatchEvaluationTable: React.FC = ({
title="Accuracy:"
value={accuracy}
precision={2}
- valueStyle={{color: ""}}
suffix="%"
/>
@@ -384,6 +436,7 @@ const ExactMatchEvaluationTable: React.FC = ({
columns={columns}
pagination={false}
rowClassName={() => "editable-row"}
+ rowKey="id"
/>
diff --git a/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx
new file mode 100644
index 0000000000..dc4b306268
--- /dev/null
+++ b/agenta-web/src/components/EvaluationTable/RegexEvaluationTable.tsx
@@ -0,0 +1,513 @@
+import {useState, useEffect, useRef} from "react"
+import type {ColumnType} from "antd/es/table"
+import {InfoCircleOutlined, LineChartOutlined} from "@ant-design/icons"
+import {
+ Button,
+ Card,
+ Col,
+ Form,
+ Input,
+ Radio,
+ Row,
+ Space,
+ Spin,
+ Statistic,
+ Table,
+ Tag,
+ Tooltip,
+ message,
+ Typography,
+} from "antd"
+import {updateEvaluationScenario, callVariant, updateEvaluation} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
+import {useRouter} from "next/router"
+import {EvaluationFlow} from "@/lib/enums"
+import {evaluateWithRegex} from "@/lib/services/evaluations"
+import {createUseStyles} from "react-jss"
+import Highlighter from "react-highlight-words"
+import {globalErrorHandler} from "@/lib/helpers/errorHandler"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {exportRegexEvaluationData} from "@/lib/helpers/evaluate"
+import {isValidRegex} from "@/lib/helpers/validators"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
+
+interface RegexEvaluationTableProps {
+ evaluation: any
+ columnsCount: number
+ evaluationScenarios: RegexEvaluationTableRow[]
+}
+
+interface RegexEvaluationTableRow {
+ id?: string
+ inputs: {
+ input_name: string
+ input_value: string
+ }[]
+ outputs: {
+ variant_id: string
+ variant_output: string
+ }[]
+ columnData0: string
+ correctAnswer: string
+ score: string
+ isMatch: boolean
+ evaluationFlow: EvaluationFlow
+}
+/**
+ *
+ * @param evaluation - Evaluation object
+ * @param evaluationScenarios - Evaluation rows
+ * @param columnsCount - Number of variants to compare face to face (per default 2)
+ * @returns
+ */
+
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: {
+ marginBottom: 20,
+ },
+ statCorrect: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ statWrong: {
+ "& .ant-statistic-content-value": {
+ color: "#cf1322",
+ },
+ },
+ form: {
+ marginBottom: 20,
+ "& .ant-form-item-has-error": {
+ marginBottom: 0,
+ },
+ },
+ regexInput: {
+ minWidth: 240,
+ },
+ infoLabel: {
+ display: "flex",
+ gap: 3,
+ alignItems: "center",
+ "& .anticon-info-circle": {
+ color: "#faad14",
+ marginTop: 2,
+ },
+ },
+})
+
+const RegexEvaluationTable: React.FC = ({
+ evaluation,
+ evaluationScenarios,
+ columnsCount,
+}) => {
+ const classes = useStyles()
+ const router = useRouter()
+ const appId = router.query.app_id as string
+ const variants = evaluation.variants
+ const variantData = useVariants(appId, variants)
+
+ const [rows, setRows] = useState([])
+ const [wrongAnswers, setWrongAnswers] = useState(0)
+ const [correctAnswers, setCorrectAnswers] = useState(0)
+ const [accuracy, setAccuracy] = useState(0)
+ const [settings, setSettings] = useState(evaluation.evaluationTypeSettings)
+ const [loading, setLoading] = useState([])
+ const [form] = Form.useForm()
+ const showError = useRef(true)
+
+ useEffect(() => {
+ if (evaluationScenarios) {
+ setRows(evaluationScenarios)
+ setLoading(Array(evaluationScenarios.length).fill(false))
+ }
+ }, [evaluationScenarios])
+
+ useEffect(() => {
+ if (correctAnswers + wrongAnswers > 0) {
+ setAccuracy((correctAnswers / (correctAnswers + wrongAnswers)) * 100)
+ } else {
+ setAccuracy(0)
+ }
+ }, [correctAnswers, wrongAnswers])
+
+ useEffect(() => {
+ const correct = rows.filter((row) => row.score === "correct").length
+ const wrong = rows.filter((row) => row.score === "wrong").length
+ const accuracy = correct + wrong > 0 ? (correct / (correct + wrong)) * 100 : 0
+
+ setCorrectAnswers(correct)
+ setWrongAnswers(wrong)
+ setAccuracy(accuracy)
+ }, [rows])
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ rowIndex: any,
+ inputFieldKey: number,
+ ) => {
+ const newRows = [...rows]
+ newRows[rowIndex].inputs[inputFieldKey].input_value = e.target.value
+ setRows(newRows)
+ }
+
+ const runAllEvaluations = async () => {
+ //validate form
+ try {
+ await form.validateFields()
+ } catch {
+ return
+ }
+ showError.current = true
+
+ const {regexPattern, regexShouldMatch} = form.getFieldsValue()
+ const promises: Promise[] = []
+
+ for (let i = 0; i < rows.length; i++) {
+ promises.push(runEvaluation(i))
+ }
+
+ Promise.all(promises)
+ .then(() => {
+ updateEvaluation(evaluation.id, {
+ evaluation_type_settings: {
+ regex_should_match: regexShouldMatch,
+ regex_pattern: regexPattern,
+ },
+ status: EvaluationFlow.EVALUATION_FINISHED,
+ }).then(() => {
+ setSettings({regexShouldMatch, regexPattern})
+ message.success("Evaluation Results Saved")
+ })
+ })
+ .catch(() => {})
+ }
+
+ const runEvaluation = async (rowIndex: number) => {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? true : val)))
+ const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
+ acc[item.input_name] = item.input_value
+ return acc
+ }, {})
+
+ const columnsDataNames = ["columnData0"]
+ for (let idx = 0; idx < columnsDataNames.length; ++idx) {
+ const columnName = columnsDataNames[idx] as keyof RegexEvaluationTableRow
+ try {
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ const {regexPattern, regexShouldMatch} = form.getFieldsValue()
+ const isCorrect = evaluateWithRegex(result, regexPattern, regexShouldMatch)
+ const evaluationScenarioId = rows[rowIndex].id
+ const score = isCorrect ? "correct" : "wrong"
+
+ if (evaluationScenarioId) {
+ await updateEvaluationScenario(
+ evaluation.id,
+ evaluationScenarioId,
+ {
+ score,
+ outputs: [{variant_id: variants[0].variantId, variant_output: result}],
+ },
+ evaluation.evaluationType,
+ )
+ }
+
+ setRowValue(rowIndex, "score", score)
+ if (isCorrect) {
+ setCorrectAnswers((prevCorrect) => prevCorrect + 1)
+ } else {
+ setWrongAnswers((prevWrong) => prevWrong + 1)
+ }
+ setRowValue(rowIndex, columnName, result)
+ } catch (err) {
+ setRowValue(rowIndex, columnName, "")
+ if (showError.current) {
+ globalErrorHandler(err)
+ showError.current = false
+ }
+ throw err
+ } finally {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? false : val)))
+ }
+ }
+ }
+
+ const setRowValue = (
+ rowIndex: number,
+ columnKey: keyof RegexEvaluationTableRow,
+ value: any,
+ ) => {
+ const newRows: any = [...rows]
+ newRows[rowIndex][columnKey] = value
+ setRows(newRows)
+ }
+
+ const dynamicColumns: ColumnType[] = Array.from(
+ {length: columnsCount},
+ (_, i) => {
+ const columnKey = `columnData${i}`
+
+ return {
+ title: (
+
+ App Variant:
+
+ {variants ? variants[i].variantName : ""}
+
+
+ ),
+ dataIndex: columnKey,
+ key: columnKey,
+ width: "25%",
+ render: (value: any, record: RegexEvaluationTableRow, ix: number) => {
+ if (loading[ix]) return "Loading..."
+
+ let outputValue = value
+ if (record.outputs && record.outputs.length > 0) {
+ outputValue = record.outputs.find(
+ (output: any) => output.variant_id === variants[i].variantId,
+ )?.variant_output
+ }
+
+ return (
+
+ )
+ },
+ }
+ },
+ )
+
+ const columns = [
+ {
+ key: "1",
+ width: "30%",
+ title: (
+
+
+ Inputs (Test set:
+ {evaluation.testset.name}
+ )
+
+
+ ),
+ dataIndex: "inputs",
+ render: (_: any, record: RegexEvaluationTableRow, rowIndex: number) => (
+
+ {evaluation.testset.testsetChatColumn
+ ? evaluation.testset.csvdata[rowIndex][
+ evaluation.testset.testsetChatColumn
+ ] || " - "
+ : record &&
+ record.inputs &&
+ record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
+ record.inputs.map((input: any, index: number) => (
+
+ handleInputChange(e, record.id, index)}
+ />
+
+ ))}
+
+ ),
+ },
+ ...dynamicColumns,
+ {
+ title: "Match / Mismatch",
+ dataIndex: "score",
+ key: "isMatch",
+ width: "25%",
+ render: (val: string, _: any, ix: number) => {
+ if (loading[ix]) return
+
+ const isCorrect = val === "correct"
+ const isMatch = settings.regexShouldMatch ? isCorrect : !isCorrect
+ return settings.regexPattern ? (
+
+ {isMatch ? "Match" : "Mismatch"}
+
+ ) : null
+ },
+ },
+ {
+ title: "Evaluation",
+ dataIndex: "score",
+ key: "evaluation",
+ width: 200,
+ align: "center" as "left" | "right" | "center",
+ render: (score: string, _: any, ix: number) => {
+ if (loading[ix]) return
+ return (
+
+ {score && (
+
+ {score}
+
+ )}
+
+ )
+ },
+ },
+ ]
+
+ return (
+
+
Regex Match / Mismatch Evaluation
+
+
+
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+
+ exportRegexEvaluationData(evaluation, rows, settings)
+ }
+ disabled={!rows?.[0]?.score}
+ >
+ Export results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {settings && (
+
+ new Promise((res, rej) =>
+ isValidRegex(value)
+ ? res("")
+ : rej("Regex pattern is not valid"),
+ ),
+ },
+ ]}
+ >
+
+
+
+ Strategy
+
+
+
+
+ }
+ rules={[{required: true, message: "Please select strategy"}]}
+ name="regexShouldMatch"
+ >
+
+ Match
+ Mismatch
+
+
+
+ )}
+
+
+
"editable-row"}
+ />
+
+
+ )
+}
+
+export default RegexEvaluationTable
diff --git a/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx
index f8fe694c68..06782fea6f 100644
--- a/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx
+++ b/agenta-web/src/components/EvaluationTable/SimilarityMatchEvaluationTable.tsx
@@ -1,14 +1,33 @@
import {useState, useEffect} from "react"
import type {ColumnType} from "antd/es/table"
import {LineChartOutlined} from "@ant-design/icons"
-import {Button, Card, Col, Input, Row, Space, Spin, Statistic, Table, Tag, message} from "antd"
-import {Variant} from "@/lib/Types"
-import {updateEvaluationScenario, callVariant} from "@/lib/services/api"
-import {useVariant} from "@/lib/hooks/useVariant"
+import {
+ Button,
+ Card,
+ Col,
+ Form,
+ Input,
+ Row,
+ Slider,
+ Space,
+ Spin,
+ Statistic,
+ Table,
+ Tag,
+ message,
+} from "antd"
+import {updateEvaluationScenario, callVariant, updateEvaluation} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
import {useRouter} from "next/router"
import {EvaluationFlow} from "@/lib/enums"
import {evaluateWithSimilarityMatch} from "@/lib/services/evaluations"
import {Typography} from "antd"
+import {createUseStyles} from "react-jss"
+import {exportSimilarityEvaluationData} from "@/lib/helpers/evaluate"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
interface SimilarityMatchEvaluationTableProps {
evaluation: any
@@ -23,7 +42,7 @@ interface SimilarityMatchEvaluationTableRow {
input_value: string
}[]
outputs: {
- variant_name: string
+ variant_id: string
variant_output: string
}[]
columnData0: string
@@ -40,45 +59,92 @@ interface SimilarityMatchEvaluationTableRow {
* @returns
*/
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: {
+ marginBottom: 20,
+ },
+ div: {
+ marginBottom: 20,
+ },
+ statCorrect: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ statWrong: {
+ "& .ant-statistic-content-value": {
+ color: "#cf1322",
+ },
+ },
+ form: {
+ marginBottom: 20,
+ "& .ant-form-item-has-error": {
+ marginBottom: 0,
+ },
+ },
+ slider: {
+ width: 200,
+ },
+})
+
const SimilarityMatchEvaluationTable: React.FC = ({
evaluation,
evaluationScenarios,
columnsCount,
}) => {
+ const classes = useStyles()
const router = useRouter()
- const appName = Array.isArray(router.query.app_name)
- ? router.query.app_name[0]
- : router.query.app_name || ""
+ const appId = router.query.app_id as string
const variants = evaluation.variants
- const variantData = variants.map((variant: Variant) => {
- const {inputParams, optParams, URIPath, isLoading, isError, error} = useVariant(
- appName,
- variant,
- )
-
- return {
- inputParams,
- optParams,
- URIPath,
- isLoading,
- isError,
- error,
- }
- })
+ const variantData = useVariants(appId, variants)
const [rows, setRows] = useState([])
const [dissimilarAnswers, setDissimilarAnswers] = useState(0)
const [similarAnswers, setSimilarAnswers] = useState(0)
const [accuracy, setAccuracy] = useState(0)
- const [loadSpinner, setLoadingSpinners] = useState(false)
-
+ const [settings, setSettings] = useState(evaluation.evaluationTypeSettings)
+ const [loading, setLoading] = useState([])
+ const [form] = Form.useForm()
const {Text} = Typography
useEffect(() => {
if (evaluationScenarios) {
- setRows(evaluationScenarios)
+ setRows(
+ evaluationScenarios.map((item) => ({
+ ...item,
+ similarity: item.outputs?.[0]?.variant_output
+ ? evaluateWithSimilarityMatch(
+ item.outputs[0].variant_output,
+ item.correctAnswer,
+ )
+ : NaN,
+ })),
+ )
+ setLoading(Array(evaluationScenarios.length).fill(false))
}
}, [evaluationScenarios])
@@ -102,7 +168,7 @@ const SimilarityMatchEvaluationTable: React.FC,
- rowIndex: number,
+ rowIndex: any,
inputFieldKey: number,
) => {
const newRows = [...rows]
@@ -111,22 +177,34 @@ const SimilarityMatchEvaluationTable: React.FC {
- // start loading spinner
- setLoadingSpinners(true)
+ //validate form
+ try {
+ await form.validateFields()
+ } catch {
+ return
+ }
+
+ const {similarityThreshold} = form.getFieldsValue()
const promises: Promise[] = []
for (let i = 0; i < rows.length; i++) {
promises.push(runEvaluation(i))
}
- Promise.all(promises)
- .then(() => {
- console.log("All functions finished.")
+ Promise.all(promises).then(() => {
+ updateEvaluation(evaluation.id, {
+ evaluation_type_settings: {
+ similarity_threshold: similarityThreshold,
+ },
+ status: EvaluationFlow.EVALUATION_FINISHED,
+ }).then(() => {
+ message.success("Evaluation Results Saved")
})
- .catch((err) => console.error("An error occurred:", err))
+ })
}
const runEvaluation = async (rowIndex: number) => {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? true : val)))
const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
acc[item.input_name] = item.input_value
return acc
@@ -134,75 +212,51 @@ const SimilarityMatchEvaluationTable: React.FC {
- setRowValue(rowIndex, columnName, "loading...")
try {
let result = await callVariant(
inputParamsDict,
- variantData[idx].inputParams,
- variantData[idx].optParams,
- variantData[idx].URIPath,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
)
+
+ const {similarityThreshold} = form.getFieldsValue()
+ const similarity = evaluateWithSimilarityMatch(result, rows[rowIndex].correctAnswer)
+ const evaluationScenarioId = rows[rowIndex].id
+ const isSimilar = similarity >= similarityThreshold ? "true" : "false"
+
+ if (evaluationScenarioId) {
+ await updateEvaluationScenario(
+ evaluation.id,
+ evaluationScenarioId,
+ {
+ score: isSimilar,
+ outputs: [{variant_id: variants[0].variantId, variant_output: result}],
+ },
+ evaluation.evaluationType,
+ )
+ }
+
+ setRowValue(rowIndex, "similarity", similarity)
+ setRowValue(rowIndex, "score", isSimilar)
+ if (isSimilar) {
+ setSimilarAnswers((prevSimilar) => prevSimilar + 1)
+ } else {
+ setDissimilarAnswers((prevDissimilar) => prevDissimilar + 1)
+ }
setRowValue(rowIndex, columnName, result)
- setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
- evaluate(rowIndex)
- } catch (e) {
+ } catch {
setRowValue(rowIndex, columnName, "")
- message.error("Oops! Something went wrong")
+ } finally {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? false : val)))
}
})
}
- /**
- *
- * @param rowNumber
- *
- * This method will:
- * 1. perform an similarity match evaluation for the given row number
- * 2. update the evaluation row with the result
- * 3. update the score column in the table
- */
- const evaluate = (rowNumber: number) => {
- const similarity = evaluateWithSimilarityMatch(
- rows[rowNumber].columnData0,
- rows[rowNumber].correctAnswer,
- )
- const isSimilar =
- similarity >= evaluation.evaluationTypeSettings.similarityThreshold ? "true" : "false"
-
- const evaluation_scenario_id = rows[rowNumber].id
-
- // TODO: we need to improve this and make it dynamic
- const appVariantNameX = variants[0].variantName
- const outputVariantX = rows[rowNumber].columnData0
-
- if (evaluation_scenario_id) {
- const data = {
- score: isSimilar,
- outputs: [{variant_name: appVariantNameX, variant_output: outputVariantX}],
- }
-
- updateEvaluationScenario(
- evaluation.id,
- evaluation_scenario_id,
- data,
- evaluation.evaluationType,
- )
- .then((data) => {
- // NOTE: both rows are set in the UI and neither of them disrupt the other
- setRowValue(rowNumber, "similarity", similarity)
- setRowValue(rowNumber, "score", data.score)
- if (isSimilar) {
- setSimilarAnswers((prevSimilar) => prevSimilar + 1)
- } else {
- setDissimilarAnswers((prevDissimilar) => prevDissimilar + 1)
- }
- })
- .catch((err) => {
- console.error(err)
- })
- }
- }
-
const setRowValue = (
rowIndex: number,
columnKey: keyof SimilarityMatchEvaluationTableRow,
@@ -222,14 +276,7 @@ const SimilarityMatchEvaluationTable: React.FC
App Variant:
-
+
{variants ? variants[i].variantName : ""}
@@ -237,14 +284,12 @@ const SimilarityMatchEvaluationTable: React.FC {
+ render: (text: any, record: SimilarityMatchEvaluationTableRow, ix: number) => {
+ if (loading[ix]) return "Loading..."
+
if (record.outputs && record.outputs.length > 0) {
const outputValue = record.outputs.find(
- (output: any) => output.variant_name === variants[i].variantName,
+ (output: any) => output.variant_id === variants[i].variantId,
)?.variant_output
return {outputValue}
}
@@ -259,19 +304,10 @@ const SimilarityMatchEvaluationTable: React.FC
+
Inputs (Test set:
-
- {evaluation.testset.name}
-
+ {evaluation.testset.name}
)
@@ -279,18 +315,22 @@ const SimilarityMatchEvaluationTable: React.FC (
),
},
@@ -300,35 +340,23 @@ const SimilarityMatchEvaluationTable: React.FC {record.correctAnswer}
,
},
{
title: "Evaluation",
- dataIndex: "evaluation",
+ dataIndex: "score",
key: "evaluation",
width: 200,
align: "center" as "left" | "right" | "center",
- render: (text: any, record: any, rowIndex: number) => {
- let tagColor = ""
- if (record.score === "true") {
- tagColor = "green"
- } else if (record.score === "false") {
- tagColor = "red"
- }
-
+ render: (score: string, _: any, ix: number) => {
+ if (loading[ix]) return
return (
-
-
-
- {!loadSpinner && rows[rowIndex].score !== "" && (
-
- {record.score}
-
- )}
-
-
-
+
+ {score && (
+
+ {score}
+
+ )}
+
)
},
},
@@ -338,32 +366,18 @@ const SimilarityMatchEvaluationTable: React.FC {
- let tagColor = ""
- if (record.score === "true") {
- tagColor = "green"
- } else if (record.score === "false") {
- tagColor = "red"
- }
+ render: (similarity: number, record: any, ix: number) => {
+ if (loading[ix]) return
- const similarity = text
- if (similarity !== undefined) {
- setTimeout(() => {
- setLoadingSpinners(false)
- }, 4000)
- }
+ const score = record.score
return (
-
-
-
- {!loadSpinner && similarity !== undefined && (
-
- {similarity.toFixed(2)}
-
- )}
-
-
-
+
+ {score && !isNaN(similarity) && (
+
+ {similarity.toFixed(2)}
+
+ )}
+
)
},
},
@@ -371,11 +385,10 @@ const SimilarityMatchEvaluationTable: React.FC
-
- Similarity match Evaluation (Threshold:{" "}
- {evaluation.evaluationTypeSettings.similarityThreshold})
-
-
+
+ Similarity match Evaluation (Threshold: {settings.similarityThreshold})
+
+
This evaluation type is calculating the similarity using Jaccard similarity.
@@ -383,31 +396,39 @@ const SimilarityMatchEvaluationTable: React.FC
- }
- size="large"
- >
- Run Evaluation
-
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+ exportSimilarityEvaluationData(evaluation, rows)}
+ disabled={!rows?.[0]?.score}
+ >
+ Export results
+
+
-
+
@@ -415,7 +436,6 @@ const SimilarityMatchEvaluationTable: React.FC
@@ -424,6 +444,27 @@ const SimilarityMatchEvaluationTable: React.FC
+
+ {settings && (
+
+ setSettings({similarityThreshold: value})}
+ />
+
+
+ )}
+
= ({
+ evaluation,
+ evaluationScenarios,
+}) => {
+ const classes = useStyles()
+ const router = useRouter()
+ const appId = router.query.app_id as string
+ const variants = evaluation.variants
+
+ const variantData = useVariants(appId, variants)
+
+ const [rows, setRows] = useState([])
+ const [evaluationStatus, setEvaluationStatus] = useState(evaluation.status)
+ const [viewMode, setViewMode] = useQueryParam("viewMode", "tabular")
+ const [accuracy, setAccuracy] = useState(0)
+
+ useEffect(() => {
+ if (evaluationScenarios) {
+ const obj = [...evaluationScenarios]
+ obj.forEach((item) =>
+ item.outputs.forEach((op) => (item[op.variant_id] = op.variant_output)),
+ )
+ setRows(obj)
+ }
+ }, [evaluationScenarios])
+
+ useEffect(() => {
+ const filtered = rows.filter((row) => row.score !== null)
+ const avg =
+ filtered.reduce((acc, val) => acc + (val.score as number), 0) / (filtered.length || 1)
+ setAccuracy(avg)
+ }, [rows])
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ id: string,
+ inputIndex: number,
+ ) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
+ const newRows = [...rows]
+ newRows[rowIndex].inputs[inputIndex].input_value = e.target.value
+ setRows(newRows)
+ }
+
+ const handleScoreChange = (id: string, score: number) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
+ const evaluation_scenario_id = rows[rowIndex].id
+
+ if (evaluation_scenario_id) {
+ setRowValue(rowIndex, "score", "loading")
+ const data = {
+ score,
+ outputs: variants.map((v: Variant) => ({
+ variant_id: v.variantId,
+ variant_output: rows[rowIndex][v.variantId],
+ })),
+ inputs: rows[rowIndex].inputs,
+ }
+
+ updateEvaluationScenarioData(evaluation_scenario_id, data)
+ }
+ }
+
+ const depouncedHandleScoreChange = useCallback(
+ debounce((...args: Parameters) => {
+ handleScoreChange(...args)
+ }, 800),
+ [handleScoreChange],
+ )
+
+ const updateEvaluationScenarioData = async (
+ id: string,
+ data: Partial,
+ showNotification: boolean = true,
+ ) => {
+ await updateEvaluationScenario(
+ evaluation.id,
+ id,
+ Object.keys(data).reduce(
+ (acc, key) => ({
+ ...acc,
+ [camelToSnake(key)]: data[key as keyof EvaluationScenario],
+ }),
+ {},
+ ),
+ evaluation.evaluationType,
+ )
+ .then(() => {
+ Object.keys(data).forEach((key) => {
+ setRowValue(
+ evaluationScenarios.findIndex((item) => item.id === id),
+ key,
+ data[key as keyof EvaluationScenario],
+ )
+ })
+ if (showNotification) message.success("Evaluation Updated!")
+ })
+ .catch(console.error)
+ }
+
+ const runAllEvaluations = async () => {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_STARTED)
+ Promise.all(rows.map((row) => runEvaluation(row.id!, rows.length - 1, false)))
+ .then(() => {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
+ message.success("Evaluations Updated!")
+ })
+ .catch((err) => console.error("An error occurred:", err))
+ }
+
+ const runEvaluation = async (
+ id: string,
+ count: number = 1,
+ showNotification: boolean = true,
+ ) => {
+ const rowIndex = rows.findIndex((row) => row.id === id)
+ const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
+ acc[item.input_name] = item.input_value
+ return acc
+ }, {})
+
+ const outputs = rows[rowIndex].outputs.reduce(
+ (acc, op) => ({...acc, [op.variant_id]: op.variant_output}),
+ {},
+ )
+
+ await Promise.all(
+ variants.map(async (variant: Variant, idx: number) => {
+ setRowValue(rowIndex, variant.variantId, "loading...")
+ try {
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ setRowValue(rowIndex, variant.variantId, result)
+ ;(outputs as KeyValuePair)[variant.variantId] = result
+ setRowValue(rowIndex, "evaluationFlow", EvaluationFlow.COMPARISON_RUN_STARTED)
+ if (idx === variants.length - 1) {
+ if (count === 1 || count === rowIndex) {
+ setEvaluationStatus(EvaluationFlow.EVALUATION_FINISHED)
+ }
+ }
+ } catch (err) {
+ console.log("Error running evaluation:", err)
+ setRowValue(rowIndex, variant.variantId, "")
+ }
+ }),
+ )
+
+ updateEvaluationScenarioData(
+ id,
+ {
+ outputs: Object.keys(outputs).map((key) => ({
+ variant_id: key,
+ variant_output: outputs[key as keyof typeof outputs],
+ })),
+ inputs: rows[rowIndex].inputs,
+ },
+ showNotification,
+ )
+ }
+
+ const setRowValue = (
+ rowIndex: number,
+ columnKey: keyof SingleModelEvaluationRow,
+ value: any,
+ ) => {
+ const newRows = [...rows]
+ newRows[rowIndex][columnKey] = value as never
+ setRows(newRows)
+ }
+
+ const dynamicColumns: ColumnType[] = variants.map(
+ (variant: Variant) => {
+ const columnKey = variant.variantId
+
+ return {
+ title: (
+
+ App Variant:
+
+ {variants ? variant.variantName : ""}
+
+
+ ),
+ dataIndex: columnKey,
+ key: columnKey,
+ width: "20%",
+ render: (text: any, record: SingleModelEvaluationRow, rowIndex: number) => {
+ if (text) return text
+ if (record.outputs && record.outputs.length > 0) {
+ const outputValue = record.outputs.find(
+ (output: any) => output.variant_id === columnKey,
+ )?.variant_output
+ return {outputValue}
+ }
+ return ""
+ },
+ }
+ },
+ )
+
+ const columns = [
+ {
+ key: "1",
+ title: (
+
+
+ Inputs (Test set:
+ {evaluation.testset.name}
+ )
+
+
+ ),
+ dataIndex: "inputs",
+ render: (text: any, record: SingleModelEvaluationRow, rowIndex: number) => (
+
+ {evaluation.testset.testsetChatColumn
+ ? evaluation.testset.csvdata[rowIndex][
+ evaluation.testset.testsetChatColumn
+ ] || " - "
+ : record &&
+ record.inputs &&
+ record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
+ record.inputs.map((input: any, index: number) => (
+
+ handleInputChange(e, record.id, index)}
+ />
+
+ ))}
+
+
+ runEvaluation(record.id!)}
+ icon={ }
+ >
+ Run
+
+
+
+ ),
+ },
+ ...dynamicColumns,
+ {
+ title: "Evaluate",
+ dataIndex: "evaluate",
+ key: "evaluate",
+ width: 200,
+ // fixed: 'right',
+ render: (text: any, record: any, rowIndex: number) => {
+ return (
+
+
+ !!item.variant_output)
+ }
+ defaultValue={record.score}
+ min={0}
+ max={100}
+ onChange={(score) => depouncedHandleScoreChange(record.id, score)}
+ />{" "}
+ / 100
+
+
+ )
+ },
+ },
+ ]
+
+ return (
+
+
{EvaluationTypeLabels.single_model_test}
+
+
+
+
+
+ Run All
+
+ exportABTestingEvaluationData(evaluation, rows)}
+ disabled={evaluationStatus !== EvaluationFlow.EVALUATION_FINISHED}
+ >
+ Export results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setViewMode(e.target.value)}
+ value={viewMode}
+ optionType="button"
+ />
+
+
+ {viewMode === "tabular" ? (
+
"editable-row"}
+ rowKey={(record) => record.id!}
+ />
+ ) : (
+ depouncedHandleScoreChange(id, score as number)}
+ onInputChange={handleInputChange}
+ updateEvaluationScenarioData={updateEvaluationScenarioData}
+ evaluation={evaluation}
+ />
+ )}
+
+ )
+}
+
+export default SingleModelEvaluationTable
diff --git a/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx b/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx
new file mode 100644
index 0000000000..4721f8f9d7
--- /dev/null
+++ b/agenta-web/src/components/EvaluationTable/WebhookEvaluationTable.tsx
@@ -0,0 +1,468 @@
+import {useState, useEffect, useRef} from "react"
+import type {ColumnType} from "antd/es/table"
+import {LineChartOutlined} from "@ant-design/icons"
+import {
+ Alert,
+ Button,
+ Card,
+ Col,
+ Form,
+ Input,
+ Row,
+ Space,
+ Spin,
+ Statistic,
+ Table,
+ Tag,
+ Tooltip,
+ Typography,
+ message,
+} from "antd"
+import {updateEvaluationScenario, callVariant, updateEvaluation} from "@/lib/services/api"
+import {useVariants} from "@/lib/hooks/useVariant"
+import {useRouter} from "next/router"
+import {EvaluationFlow} from "@/lib/enums"
+import {evaluateWithWebhook} from "@/lib/services/evaluations"
+import {createUseStyles} from "react-jss"
+import {globalErrorHandler} from "@/lib/helpers/errorHandler"
+import {isValidUrl} from "@/lib/helpers/validators"
+import SecondaryButton from "../SecondaryButton/SecondaryButton"
+import {exportWebhookEvaluationData} from "@/lib/helpers/evaluate"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+
+const {Title} = Typography
+
+interface WebhookEvaluationTableProps {
+ evaluation: any
+ columnsCount: number
+ evaluationScenarios: WebhookEvaluationTableRow[]
+}
+
+interface WebhookEvaluationTableRow {
+ id?: string
+ inputs: {
+ input_name: string
+ input_value: string
+ }[]
+ outputs: {
+ variant_id: string
+ variant_output: string
+ }[]
+ columnData0: string
+ correctAnswer: string
+ score: string
+ isMatch: boolean
+ evaluationFlow: EvaluationFlow
+}
+/**
+ *
+ * @param evaluation - Evaluation object
+ * @param evaluationScenarios - Evaluation rows
+ * @param columnsCount - Number of variants to compare face to face (per default 2)
+ * @returns
+ */
+
+const useStyles = createUseStyles({
+ appVariant: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ inputTestContainer: {
+ display: "flex",
+ justifyContent: "space-between",
+ },
+ inputTest: {
+ backgroundColor: "rgb(201 255 216)",
+ color: "rgb(0 0 0)",
+ padding: 4,
+ borderRadius: 5,
+ },
+ recordInput: {
+ marginBottom: 10,
+ },
+ tag: {
+ fontSize: "14px",
+ },
+ card: {
+ marginBottom: 20,
+ },
+ infoBox: {
+ marginBottom: 20,
+ },
+ pre: {
+ position: "relative",
+ overflow: "auto",
+ },
+ statCorrect: {
+ "& .ant-statistic-content-value": {
+ color: "#3f8600",
+ },
+ },
+ statWrong: {
+ "& .ant-statistic-content-value": {
+ color: "#cf1322",
+ },
+ },
+ form: {
+ marginBottom: 20,
+ "& .ant-form-item-has-error": {
+ marginBottom: 0,
+ },
+ },
+})
+
+const WebhookEvaluationTable: React.FC = ({
+ evaluation,
+ evaluationScenarios,
+ columnsCount,
+}) => {
+ const classes = useStyles()
+ const router = useRouter()
+ const appId = router.query.app_id as string
+ const variants = evaluation.variants
+ const variantData = useVariants(appId, variants)
+
+ const [rows, setRows] = useState([])
+ const [accuracy, setAccuracy] = useState(0)
+ const [settings, setSettings] = useState(evaluation.evaluationTypeSettings)
+ const [loading, setLoading] = useState([])
+ const [form] = Form.useForm()
+ const showError = useRef(true)
+
+ useEffect(() => {
+ if (evaluationScenarios) {
+ setRows(evaluationScenarios)
+ setLoading(Array(evaluationScenarios.length).fill(false))
+ }
+ }, [evaluationScenarios])
+
+ useEffect(() => {
+ const scores = rows.filter((item) => !isNaN(+item.score)).map((item) => +item.score)
+ const avg = scores.reduce((acc, val) => acc + val, 0) / (scores.length || 1)
+ setAccuracy(avg * 100)
+ }, [rows])
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ rowIndex: any,
+ inputFieldKey: number,
+ ) => {
+ const newRows = [...rows]
+ newRows[rowIndex].inputs[inputFieldKey].input_value = e.target.value
+ setRows(newRows)
+ }
+
+ const runAllEvaluations = async () => {
+ //validate form
+ try {
+ await form.validateFields()
+ } catch {
+ return
+ }
+ showError.current = true
+
+ const {webhookUrl} = form.getFieldsValue()
+ const promises: Promise[] = []
+
+ for (let i = 0; i < rows.length; i++) {
+ promises.push(runEvaluation(i))
+ }
+
+ Promise.all(promises)
+ .then(() => {
+ updateEvaluation(evaluation.id, {
+ evaluation_type_settings: {
+ webhook_url: webhookUrl,
+ },
+ status: EvaluationFlow.EVALUATION_FINISHED,
+ }).then(() => {
+ setSettings({webhookUrl})
+ message.success("Evaluation Results Saved")
+ })
+ })
+ .catch(() => {})
+ }
+
+ const runEvaluation = async (rowIndex: number) => {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? true : val)))
+ const inputParamsDict = rows[rowIndex].inputs.reduce((acc: {[key: string]: any}, item) => {
+ acc[item.input_name] = item.input_value
+ return acc
+ }, {})
+
+ const columnsDataNames = ["columnData0"]
+ for (let idx = 0; idx < columnsDataNames.length; ++idx) {
+ const columnName = columnsDataNames[idx] as keyof WebhookEvaluationTableRow
+ try {
+ let result = await callVariant(
+ inputParamsDict,
+ variantData[idx].inputParams!,
+ variantData[idx].optParams!,
+ appId || "",
+ variants[idx].baseId || "",
+ variantData[idx].isChatVariant
+ ? testsetRowToChatMessages(evaluation.testset.csvdata[rowIndex], false)
+ : [],
+ )
+
+ const {webhookUrl} = form.getFieldsValue()
+ const score = await evaluateWithWebhook(webhookUrl, {
+ input_vars: inputParamsDict,
+ output: result,
+ correct_answer: rows[rowIndex].correctAnswer || null,
+ })
+ const evaluationScenarioId = rows[rowIndex].id
+
+ if (evaluationScenarioId) {
+ await updateEvaluationScenario(
+ evaluation.id,
+ evaluationScenarioId,
+ {
+ score,
+ outputs: [{variant_id: variants[0].variantId, variant_output: result}],
+ },
+ evaluation.evaluationType,
+ )
+ }
+
+ setRowValue(rowIndex, "score", score)
+ setRowValue(rowIndex, columnName, result)
+ } catch (err) {
+ setRowValue(rowIndex, columnName, "")
+ if (showError.current) {
+ globalErrorHandler(err)
+ showError.current = false
+ }
+ throw err
+ } finally {
+ setLoading((prev) => prev.map((val, i) => (i === rowIndex ? false : val)))
+ }
+ }
+ }
+
+ const setRowValue = (
+ rowIndex: number,
+ columnKey: keyof WebhookEvaluationTableRow,
+ value: any,
+ ) => {
+ const newRows: any = [...rows]
+ newRows[rowIndex][columnKey] = value
+ setRows(newRows)
+ }
+
+ const dynamicColumns: ColumnType[] = Array.from(
+ {length: columnsCount},
+ (_, i) => {
+ const columnKey = `columnData${i}`
+
+ return {
+ title: (
+
+ App Variant:
+
+ {variants ? variants[i].variantName : ""}
+
+
+ ),
+ dataIndex: columnKey,
+ key: columnKey,
+ width: "25%",
+ render: (value: any, record: WebhookEvaluationTableRow, ix: number) => {
+ if (loading[ix]) return "Loading..."
+
+ let outputValue = value
+ if (record.outputs && record.outputs.length > 0) {
+ outputValue = record.outputs.find(
+ (output: any) => output.variant_id === variants[i].variantId,
+ )?.variant_output
+ }
+
+ return outputValue
+ },
+ }
+ },
+ )
+
+ const columns = [
+ {
+ key: "1",
+ width: "30%",
+ title: (
+
+
+ Inputs (Test set:
+ {evaluation.testset.name}
+ )
+
+
+ ),
+ dataIndex: "inputs",
+ render: (_: any, record: WebhookEvaluationTableRow, rowIndex: number) => (
+
+ {evaluation.testset.testsetChatColumn
+ ? evaluation.testset.csvdata[rowIndex][
+ evaluation.testset.testsetChatColumn
+ ] || " - "
+ : record &&
+ record.inputs &&
+ record.inputs.length && // initial value of inputs is array with 1 element and variantInputs could contain more than 1 element
+ record.inputs.map((input: any, index: number) => (
+
+ handleInputChange(e, record.id, index)}
+ />
+
+ ))}
+
+ ),
+ },
+ ...dynamicColumns,
+ {
+ title: "Correct Answer",
+ dataIndex: "correctAnswer",
+ key: "correctAnswer",
+ width: "25%",
+ },
+ {
+ title: "Evaluation",
+ dataIndex: "score",
+ key: "evaluation",
+ width: 200,
+ align: "center" as "left" | "right" | "center",
+ render: (score: string, _: any, ix: number) => {
+ if (loading[ix]) return
+ return (
+
+ {score && (
+
+ {(+score).toFixed(2)}
+
+ )}
+
+ )
+ },
+ },
+ ]
+
+ return (
+
+
Webhook URL Evaluation
+
+ The webhook URL you provide will be called with an HTTP POST request.
+ The request body will contain the following JSON object:
+
+
+ {`{
+ "input_vars": { // Key/value pairs for each variable in the Test Suite / Prompt
+ "var_1": "value_1",
+ "var_2": "value_2",
+ ...
+ },
+ "output": string, // The LLM's output
+ "correct_answer": string | null // The correct answer, if available
+}`}
+
+
+ Thre response of the payload should contain the following JSON object:
+
+
+ {`{
+ "score": number // Evaluation score between 0 and 1, 0 being "bad" and 1 being "good"
+}`}
+
+
+
+ NOTE: Your webhook should allow CORS request from our domain in
+ the response headers
+
+
+ }
+ type="info"
+ showIcon
+ />
+
+
+
+
+ }
+ size="large"
+ >
+ Run Evaluation
+
+ exportWebhookEvaluationData(evaluation, rows)}
+ disabled={!rows?.[0]?.score}
+ >
+ Export results
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {settings && (
+
+ new Promise((res, rej) =>
+ isValidUrl(value)
+ ? res("")
+ : rej("Please enter a valid url"),
+ ),
+ },
+ ]}
+ >
+
+
+
+ )}
+
+
+
"editable-row"}
+ />
+
+
+ )
+}
+
+export default WebhookEvaluationTable
diff --git a/agenta-web/src/components/Evaluations/AutomaticEvaluationResult.tsx b/agenta-web/src/components/Evaluations/AutomaticEvaluationResult.tsx
new file mode 100644
index 0000000000..29ee9668db
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/AutomaticEvaluationResult.tsx
@@ -0,0 +1,322 @@
+import {deleteEvaluations, fetchEvaluationResults, loadEvaluations} from "@/lib/services/api"
+import {Button, Collapse, Statistic, Table, Typography} from "antd"
+import {useRouter} from "next/router"
+import {useEffect, useState} from "react"
+import {ColumnsType} from "antd/es/table"
+import {Evaluation, GenericObject} from "@/lib/Types"
+import {DeleteOutlined} from "@ant-design/icons"
+import {EvaluationTypeLabels} from "@/lib/helpers/utils"
+import {EvaluationFlow, EvaluationType} from "@/lib/enums"
+import {createUseStyles} from "react-jss"
+import {useAppTheme} from "../Layout/ThemeContextProvider"
+import {calculateResultsDataAvg} from "@/lib/helpers/evaluate"
+
+interface EvaluationListTableDataType {
+ key: string
+ variants: string[]
+ testset: {
+ _id: string
+ name: string
+ }
+ evaluationType: string
+ status: EvaluationFlow
+ scoresData: {
+ nb_of_rows: number
+ wrong?: GenericObject[]
+ correct?: GenericObject[]
+ true?: GenericObject[]
+ false?: GenericObject[]
+ variant: string[]
+ }
+ avgScore: number
+ custom_code_eval_id: string
+ resultsData: {[key: string]: number}
+ createdAt: string
+}
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
+
+const useStyles = createUseStyles({
+ container: {
+ marginBottom: 20,
+ "& svg": {
+ color: "red",
+ },
+ },
+ collapse: ({themeMode}: StyleProps) => ({
+ margin: "10px 0",
+ "& .ant-collapse-header": {
+ alignItems: "center !important",
+ padding: "0px 20px !important",
+ borderTopLeftRadius: "10px !important",
+ borderTopRightRadius: "10px !important",
+ background: themeMode === "dark" ? "#1d1d1d" : "#f8f8f8",
+ },
+ }),
+ stat: {
+ "& .ant-statistic-content-value": {
+ fontSize: 20,
+ color: "#1677ff",
+ },
+ "& .ant-statistic-content-suffix": {
+ fontSize: 20,
+ color: "#1677ff",
+ },
+ },
+})
+
+const {Title} = Typography
+
+export default function AutomaticEvaluationResult() {
+ const router = useRouter()
+ const [evaluationsList, setEvaluationsList] = useState([])
+ const [selectedRowKeys, setSelectedRowKeys] = useState([])
+ const [selectionType] = useState<"checkbox" | "radio">("checkbox")
+ const {appTheme} = useAppTheme()
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
+
+ const app_id = router.query.app_id?.toString() || ""
+
+ useEffect(() => {
+ if (!app_id) {
+ return
+ }
+
+ const fetchEvaluations = async () => {
+ try {
+ const evals: Evaluation[] = await loadEvaluations(app_id)
+ const results = await Promise.all(evals.map((e) => fetchEvaluationResults(e.id)))
+ const newEvals = results.map((result, ix) => {
+ const item = evals[ix]
+ if (
+ [
+ EvaluationType.auto_exact_match,
+ EvaluationType.auto_similarity_match,
+ EvaluationType.auto_regex_test,
+ EvaluationType.auto_ai_critique,
+ EvaluationType.custom_code_run,
+ EvaluationType.auto_webhook_test,
+ EvaluationType.single_model_test,
+ ].includes(item.evaluationType)
+ ) {
+ return {
+ key: item.id,
+ createdAt: item.createdAt,
+ variants: item.variants,
+ scoresData: result.scores_data,
+ evaluationType: item.evaluationType,
+ status: item.status,
+ testset: item.testset,
+ custom_code_eval_id: item.evaluationTypeSettings.customCodeEvaluationId,
+ resultsData: result.results_data,
+ avgScore: result.avg_score,
+ }
+ }
+ })
+
+ setEvaluationsList(
+ newEvals
+ .filter((evaluation) => evaluation !== undefined)
+ .filter(
+ (item: any) =>
+ item.resultsData !== undefined ||
+ !(Object.keys(item.scoresData || {}).length === 0) ||
+ item.avgScore !== undefined,
+ ) as any,
+ )
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ fetchEvaluations()
+ }, [app_id])
+
+ const onCompleteEvaluation = (evaluation: any) => {
+ // TODO: improve type
+ const evaluationType =
+ EvaluationType[evaluation.evaluationType as keyof typeof EvaluationType]
+
+ if (evaluationType === EvaluationType.auto_exact_match) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/auto_exact_match`)
+ } else if (evaluationType === EvaluationType.auto_similarity_match) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/auto_similarity_match`)
+ } else if (evaluationType === EvaluationType.auto_regex_test) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/auto_regex_test`)
+ } else if (evaluationType === EvaluationType.auto_webhook_test) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/auto_webhook_test`)
+ } else if (evaluationType === EvaluationType.single_model_test) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/single_model_test`)
+ } else if (evaluationType === EvaluationType.auto_ai_critique) {
+ router.push(`/apps/${app_id}/evaluations/${evaluation.key}/auto_ai_critique`)
+ } else if (evaluationType === EvaluationType.custom_code_run) {
+ router.push(
+ `/apps/${app_id}/evaluations/${evaluation.key}/custom_code_run?custom_eval_id=${evaluation.custom_code_eval_id}`,
+ )
+ }
+ }
+
+ const columns: ColumnsType = [
+ {
+ title: "Variant",
+ dataIndex: "variants",
+ key: "variants",
+ render: (value) => {
+ return (
+
+ {value[0].variantName}
+
+ )
+ },
+ },
+ {
+ title: "Test set",
+ dataIndex: "testsetName",
+ key: "testsetName",
+ render: (value: any, record: EvaluationListTableDataType, index: number) => {
+ return {record.testset.name}
+ },
+ },
+ {
+ title: "Evaluation type",
+ dataIndex: "evaluationType",
+ key: "evaluationType",
+ width: "300",
+ render: (value: string) => {
+ const evaluationType = EvaluationType[value as keyof typeof EvaluationType]
+ const label = EvaluationTypeLabels[evaluationType]
+ return {label}
+ },
+ },
+ {
+ title: "Average score",
+ dataIndex: "averageScore",
+ key: "averageScore",
+ render: (value: any, record: EvaluationListTableDataType, index: number) => {
+ let score = 0
+ if (record.scoresData) {
+ score =
+ ((record.scoresData.correct?.length ||
+ record.scoresData.true?.length ||
+ 0) /
+ record.scoresData.nb_of_rows) *
+ 100
+ } else if (record.resultsData) {
+ const multiplier = {
+ [EvaluationType.auto_webhook_test]: 100,
+ [EvaluationType.single_model_test]: 1,
+ }
+ score = calculateResultsDataAvg(
+ record.resultsData,
+ multiplier[record.evaluationType as keyof typeof multiplier],
+ )
+ score = isNaN(score) ? 0 : score
+ } else if (record.avgScore) {
+ score = record.avgScore * 100
+ }
+
+ return (
+
+
+
+ )
+ },
+ },
+ {
+ title: "Created at",
+ dataIndex: "createdAt",
+ key: "createdAt",
+ width: "300",
+ },
+ {
+ title: "Action",
+ dataIndex: "action",
+ key: "action",
+ render: (value: any, record: EvaluationListTableDataType, index: number) => {
+ let actionText = "View evaluation"
+ if (record.status !== EvaluationFlow.EVALUATION_FINISHED) {
+ actionText = "Continue evaluation"
+ }
+ return (
+
+ onCompleteEvaluation(record)}>
+ {actionText}
+
+
+ )
+ },
+ },
+ ]
+
+ const rowSelection = {
+ onChange: (selectedRowKeys: React.Key[], selectedRows: EvaluationListTableDataType[]) => {
+ setSelectedRowKeys(selectedRowKeys)
+ },
+ }
+
+ const onDelete = async () => {
+ const evaluationsIds = selectedRowKeys.map((key) => key.toString())
+ try {
+ await deleteEvaluations(evaluationsIds)
+ setEvaluationsList((prevEvaluationsList) =>
+ prevEvaluationsList.filter(
+ (evaluation) => !evaluationsIds.includes(evaluation.key),
+ ),
+ )
+
+ setSelectedRowKeys([])
+ } catch {
+ } finally {
+ }
+ }
+
+ const items = [
+ {
+ key: "1",
+ label: (
+
+
Evaluation Results
+
+ ),
+ children: (
+
+ ),
+ },
+ ]
+
+ return (
+
+ )
+}
diff --git a/agenta-web/src/components/Evaluations/CustomPythonCode.tsx b/agenta-web/src/components/Evaluations/CustomPythonCode.tsx
new file mode 100644
index 0000000000..2e620dbff2
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/CustomPythonCode.tsx
@@ -0,0 +1,231 @@
+import React, {useState, useEffect} from "react"
+import {useRouter} from "next/router"
+import {Input, Form, Button, Row, Col, Typography, notification} from "antd"
+import {CreateCustomEvaluationSuccessResponse} from "@/lib/Types"
+import {
+ saveCustomCodeEvaluation,
+ fetchCustomEvaluationNames,
+ editCustomEvaluationDetail,
+} from "@/lib/services/api"
+import Editor from "@monaco-editor/react"
+
+interface ICustomPythonProps {
+ classes: any
+ appId: string
+ appTheme: string
+ editMode: boolean
+ editCode?: string
+ editName?: string
+ editId?: string
+}
+
+interface ICustomEvalNames {
+ id: string
+ evaluation_name: string
+}
+
+const CustomPythonCode: React.FC = ({
+ classes,
+ appId,
+ appTheme,
+ editMode,
+ editCode = "",
+ editName = "",
+ editId = "",
+}) => {
+ const {Title} = Typography
+ const [form] = Form.useForm()
+ const router = useRouter()
+
+ const [submitting, setSubmittingData] = useState(false)
+ const [evalNames, setEvalNames] = useState([])
+ const [evalNameExist, setEvalNameExist] = useState(false)
+
+ let prevKey = ""
+ const showNotification = (config: Parameters[0]) => {
+ if (prevKey) notification.destroy(prevKey)
+ prevKey = (config.key || "") as string
+ notification.open(config)
+ }
+
+ useEffect(() => {
+ const evaluationNames = async () => {
+ const response: any = await fetchCustomEvaluationNames(appId)
+ if (response.status === 200) {
+ setEvalNames(response.data)
+ }
+ }
+
+ evaluationNames()
+ }, [appId])
+
+ const handlerToSubmitFormData = async (values: any) => {
+ setSubmittingData(true)
+ const data = {
+ evaluation_name: values.evaluationName, // TODO: Change to evaluation id!
+ python_code: values.pythonCode,
+ app_id: appId,
+ }
+ const response = editMode
+ ? await editCustomEvaluationDetail(editId, data)
+ : await saveCustomCodeEvaluation(data)
+ if (response.status === 200) {
+ const data: CreateCustomEvaluationSuccessResponse = response.data
+
+ // Disable submitting form data
+ setSubmittingData(false)
+ showNotification({
+ type: "success",
+ message: "Custom Evaluation",
+ description: data.message,
+ key: data.evaluation_id,
+ })
+
+ // Reset form fields and redirect user to evaluations page
+ form.resetFields()
+ router.push(`/apps/${appId}/evaluations/`)
+ }
+ }
+
+ const isSaveButtonDisabled = () => {
+ return (
+ evalNameExist ||
+ !form.isFieldsTouched(true) ||
+ form.getFieldsError().filter(({errors}) => errors.length).length > 0
+ )
+ }
+
+ const isEditButtonDisabled = () => {
+ return evalNameExist
+ }
+
+ const pythonDefaultEvalCode = () => {
+ if (editMode) {
+ return editCode
+ } else {
+ return `from typing import Dict
+
+def evaluate(
+ app_params: Dict[str, str],
+ inputs: Dict[str, str],
+ output: str,
+ correct_answer: str
+) -> float:
+ # ...
+ return 0.75 # Replace with your calculated score`
+ }
+ }
+
+ const switchEditorThemeBasedOnTheme = () => {
+ if (appTheme == "light") {
+ return "vs-light"
+ } else if (appTheme == "dark") {
+ return "vs-dark"
+ }
+ }
+
+ const checkForEvaluationName = async () => {
+ const evalName = form.getFieldValue("evaluationName")
+ if (evalName === editName) {
+ return
+ } else if (evalNames.map((e) => e.evaluation_name).includes(evalName)) {
+ showNotification({
+ type: "error",
+ message: "Custom Evaluation",
+ duration: 5,
+ description: "Evaluation name already exist. ",
+ })
+ setEvalNameExist(true)
+ } else {
+ setEvalNameExist(false)
+ }
+ }
+
+ return (
+
+
+ {editMode ? "Edit Python Code Evaluation" : "Save Python Code Evaluation"}
+
+
+
+ )
+}
+
+export default CustomPythonCode
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationCard.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationCard.tsx
new file mode 100644
index 0000000000..e05adecc9d
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationCard.tsx
@@ -0,0 +1,71 @@
+import {Variant} from "@/lib/Types"
+import React from "react"
+import {createUseStyles} from "react-jss"
+import EvaluationVariantCard from "./EvaluationVariantCard"
+import {ABTestingEvaluationTableRow} from "@/components/EvaluationTable/ABTestingEvaluationTable"
+import EvaluationChatResponse from "./EvaluationChatResponse"
+
+const useStyles = createUseStyles({
+ root: {
+ display: "flex",
+ gap: "1rem",
+ flexWrap: "wrap",
+ },
+})
+
+interface Props {
+ evaluationScenario: ABTestingEvaluationTableRow
+ variants: Variant[]
+ isChat?: boolean
+ showVariantName?: boolean
+}
+
+const EvaluationCard: React.FC = ({
+ evaluationScenario,
+ variants,
+ isChat,
+ showVariantName = true,
+}) => {
+ const classes = useStyles()
+
+ return (
+
+ {variants.map((variant, ix) =>
+ isChat ? (
+ item.variant_id)
+ ?.variant_output ||
+ ""
+ }
+ index={ix}
+ showVariantName={showVariantName}
+ />
+ ) : (
+ item.variant_id)
+ ?.variant_output ||
+ ""
+ }
+ index={ix}
+ showVariantName={showVariantName}
+ //random image from unsplash
+ // outputImg={`https://source.unsplash.com/random/?sig=${ix}`}
+ />
+ ),
+ )}
+
+ )
+}
+
+export default EvaluationCard
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationChatResponse.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationChatResponse.tsx
new file mode 100644
index 0000000000..fbf83ab4e3
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationChatResponse.tsx
@@ -0,0 +1,65 @@
+import {ChatRole, Variant} from "@/lib/Types"
+import React from "react"
+import {createUseStyles} from "react-jss"
+import {VARIANT_COLORS} from "."
+import {Space, Typography} from "antd"
+import ChatInputs from "@/components/ChatInputs/ChatInputs"
+import {v4 as uuidv4} from "uuid"
+
+const useStyles = createUseStyles({
+ title: {
+ fontSize: 20,
+ textAlign: "center",
+ },
+ variantType: {
+ borderRadius: "50%",
+ border: `1.5px solid`,
+ width: 28,
+ aspectRatio: "1/1",
+ display: "grid",
+ placeItems: "center",
+ "& .ant-typography": {
+ fontSize: 16,
+ },
+ },
+})
+
+type Props = {
+ variant: Variant
+ outputText?: string
+ index?: number
+ showVariantName?: boolean
+}
+
+const EvaluationChatResponse: React.FC = ({
+ variant,
+ outputText,
+ index = 0,
+ showVariantName = true,
+}) => {
+ const classes = useStyles()
+ const color = VARIANT_COLORS[index]
+
+ return (
+
+ {showVariantName && (
+
+
+
+ {String.fromCharCode(65 + index)}
+
+
+
+ {variant.variantName}
+
+
+ )}
+
+
+ )
+}
+
+export default EvaluationChatResponse
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationInputs.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationInputs.tsx
new file mode 100644
index 0000000000..09e27dfe0f
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationInputs.tsx
@@ -0,0 +1,49 @@
+import {EvaluationScenario} from "@/lib/Types"
+import {Input, Typography} from "antd"
+import React from "react"
+import {createUseStyles} from "react-jss"
+
+const useStyles = createUseStyles({
+ root: {
+ display: "flex",
+ gap: "1rem",
+ flexWrap: "wrap",
+ },
+ inputRow: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "0.25rem",
+ "& .ant-typography": {
+ textTransform: "capitalize",
+ },
+ "& input": {
+ width: 200,
+ },
+ },
+})
+
+interface Props {
+ evaluationScenario: EvaluationScenario
+ onInputChange: Function
+}
+
+const EvaluationInputs: React.FC = ({evaluationScenario, onInputChange}) => {
+ const classes = useStyles()
+
+ return (
+
+ {evaluationScenario.inputs.map((ip, ix) => (
+
+ {ip.input_name}:
+ onInputChange(e, evaluationScenario.id, ix)}
+ />
+
+ ))}
+
+ )
+}
+
+export default EvaluationInputs
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVariantCard.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVariantCard.tsx
new file mode 100644
index 0000000000..3d78fd13a4
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVariantCard.tsx
@@ -0,0 +1,100 @@
+import {useAppTheme} from "@/components/Layout/ThemeContextProvider"
+import {Variant} from "@/lib/Types"
+import {Typography} from "antd"
+import React from "react"
+import {createUseStyles} from "react-jss"
+import {VARIANT_COLORS} from "."
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
+
+const useStyles = createUseStyles({
+ root: ({themeMode}: StyleProps) => ({
+ flex: 1,
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ gap: "0.75rem",
+ border: `1px solid ${themeMode === "dark" ? "#424242" : "#d9d9d9"}`,
+ padding: "0.75rem",
+ paddingTop: "1.25rem",
+ borderRadius: 6,
+ "& img": {
+ maxHeight: 300,
+ width: "100%",
+ objectFit: "contain",
+ borderRadius: "inherit",
+ },
+ position: "relative",
+ }),
+ title: {
+ fontSize: 20,
+ textAlign: "center",
+ },
+ output: {
+ whiteSpace: "pre-line",
+ position: "relative",
+ maxHeight: 300,
+ overflow: "auto",
+ },
+ variantType: {
+ position: "absolute",
+ top: 10,
+ left: 10,
+ borderRadius: "50%",
+ border: `1.5px solid`,
+ width: 32,
+ aspectRatio: "1/1",
+ display: "grid",
+ placeItems: "center",
+
+ "& .ant-typography": {
+ fontSize: 18,
+ },
+ },
+})
+
+type Props = {
+ variant: Variant
+ outputText?: string
+ outputImg?: string
+ index?: number
+ showVariantName?: boolean
+}
+
+const EvaluationVariantCard: React.FC = ({
+ variant,
+ outputText,
+ outputImg,
+ index = 0,
+ showVariantName = true,
+}) => {
+ const {appTheme} = useAppTheme()
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
+ const color = VARIANT_COLORS[index]
+
+ return (
+
+ {showVariantName && (
+ <>
+ {" "}
+
+
+ {String.fromCharCode(65 + index)}
+
+
+
+ {variant.variantName}
+ {" "}
+ >
+ )}
+ {outputImg &&
}
+
+ {outputText || Click the "Run" icon to get variant output }
+
+
+ )
+}
+
+export default EvaluationVariantCard
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVotePanel.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVotePanel.tsx
new file mode 100644
index 0000000000..8c46db1af8
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/EvaluationVotePanel.tsx
@@ -0,0 +1,272 @@
+import {Variant} from "@/lib/Types"
+import {Button, ConfigProvider, InputNumber, Spin, Typography, theme} from "antd"
+import React from "react"
+import {createUseStyles} from "react-jss"
+import {VARIANT_COLORS} from "."
+
+const useStyles = createUseStyles({
+ root: {
+ display: "flex",
+ justifyContent: "center",
+ width: "100%",
+ },
+ btnRow: {
+ display: "flex",
+ gap: "0.5rem",
+ },
+ gradeRoot: {
+ display: "flex",
+ alignItems: "center",
+ gap: "1.5rem",
+ },
+ variantName: {
+ display: "inline-block",
+ marginBottom: "0.25rem",
+ },
+ btnsDividerHorizontal: {
+ height: 30,
+ borderRight: "1.2px solid",
+ alignSelf: "center",
+ margin: "0 4px",
+ },
+ btnsDividerVertical: {
+ width: 120,
+ borderBottom: "1.2px solid",
+ alignSelf: "center",
+ margin: "4px 0",
+ },
+})
+
+interface CommonProps {
+ onChange: (value: T) => void
+ value?: T
+ vertical?: boolean
+}
+
+type BinaryVoteProps = CommonProps
+
+const BinaryVote: React.FC = ({onChange, value, vertical}) => {
+ const classes = useStyles()
+
+ const getOnClick = (isGood: boolean) => () => {
+ onChange(isGood)
+ }
+
+ return (
+
+
+ Good
+
+
+ Bad
+
+
+ )
+}
+
+type ComparisonVoteProps = {
+ variants: Variant[]
+} & CommonProps
+
+const ComparisonVote: React.FC = ({variants, onChange, value, vertical}) => {
+ const classes = useStyles()
+ const {token} = theme.useToken()
+ const badId = "0"
+
+ const getOnClick = (variantId: string) => () => {
+ onChange(variantId)
+ }
+
+ return (
+
+ {variants.map((variant, ix) => (
+
+
+ {String.fromCharCode(65 + ix)}: {variant.variantName}
+
+
+ ))}
+
+
+ Both are bad
+
+
+ )
+}
+
+type GradingVoteProps = {
+ variants: Variant[]
+ maxGrade?: number
+} & CommonProps<
+ {
+ grade: number | null
+ variantId: string
+ }[]
+>
+
+const GradingVote: React.FC = ({
+ variants,
+ onChange,
+ value = [],
+ maxGrade = 5,
+ vertical,
+}) => {
+ const classes = useStyles()
+
+ const getOnClick = (variantId: string, grade: number) => () => {
+ onChange(
+ variants.map((variant) => ({
+ variantId: variant.variantId,
+ grade: variant.variantId === variantId ? grade : null,
+ })),
+ )
+ }
+
+ return (
+
+ {variants.map((variant) => (
+
+
+ {variant.variantName}
+
+
+ {Array.from({length: maxGrade}, (_, i) => i + 1).map((grade) => (
+ item.variantId === variant.variantId)
+ ?.grade === grade
+ ? "primary"
+ : undefined
+ }
+ >
+ {grade}
+
+ ))}
+
+
+ ))}
+
+ )
+}
+
+type NumericScoreVoteProps = {
+ variants: Variant[]
+ min?: number
+ max?: number
+ showVariantName?: boolean
+} & CommonProps<
+ {
+ score: number | null
+ variantId: string
+ }[]
+>
+
+const NumericScoreVote: React.FC = ({
+ variants,
+ onChange,
+ value = [],
+ min = 0,
+ max = 100,
+ vertical,
+ showVariantName = true,
+}) => {
+ const classes = useStyles()
+
+ const _onChange = (variantId: string, score: number | null) => {
+ onChange(
+ variants.map((variant) => ({
+ variantId: variant.variantId,
+ score: variant.variantId === variantId ? score : null,
+ })),
+ )
+ }
+
+ return (
+
+ {variants.map((variant) => (
+
+ {showVariantName && (
+
+ {variant.variantName}
+
+ )}
+
+ item.variantId === variant.variantId)?.score ||
+ undefined
+ }
+ min={min}
+ max={max}
+ onChange={(score) => _onChange(variant.variantId, score)}
+ />{" "}
+ / {max}
+
+
+ ))}
+
+ )
+}
+
+type Props =
+ | ({
+ type: "binary"
+ } & BinaryVoteProps)
+ | ({
+ type: "comparison"
+ } & ComparisonVoteProps)
+ | ({
+ type: "grading"
+ } & GradingVoteProps)
+ | ({
+ type: "numeric"
+ } & NumericScoreVoteProps)
+
+const EvaluationVotePanel: React.FC = ({type, loading, ...props}) => {
+ const classes = useStyles()
+
+ return (
+
+
+ {type === "binary" ? (
+
+ ) : type === "comparison" ? (
+
+ ) : type === "grading" ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export default EvaluationVotePanel
diff --git a/agenta-web/src/components/Evaluations/EvaluationCardView/index.tsx b/agenta-web/src/components/Evaluations/EvaluationCardView/index.tsx
new file mode 100644
index 0000000000..53b587ee34
--- /dev/null
+++ b/agenta-web/src/components/Evaluations/EvaluationCardView/index.tsx
@@ -0,0 +1,478 @@
+import {useQueryParam} from "@/hooks/useQuery"
+import {ChatMessage, Evaluation, EvaluationScenario, Variant} from "@/lib/Types"
+import {
+ LeftOutlined,
+ PlayCircleOutlined,
+ PushpinFilled,
+ PushpinOutlined,
+ QuestionCircleOutlined,
+ RightOutlined,
+} from "@ant-design/icons"
+import {Button, Empty, Input, Space, Tooltip, Typography, theme} from "antd"
+import React, {useCallback, useEffect, useMemo, useRef} from "react"
+import {createUseStyles} from "react-jss"
+import EvaluationVotePanel from "./EvaluationVotePanel"
+import EvaluationCard from "./EvaluationCard"
+import EvaluationInputs from "./EvaluationInputs"
+import {ABTestingEvaluationTableRow} from "@/components/EvaluationTable/ABTestingEvaluationTable"
+import AlertPopup from "@/components/AlertPopup/AlertPopup"
+import {useLocalStorage} from "usehooks-ts"
+import ChatInputs from "@/components/ChatInputs/ChatInputs"
+import {testsetRowToChatMessages} from "@/lib/helpers/testset"
+import {safeParse} from "@/lib/helpers/utils"
+import {debounce} from "lodash"
+import {EvaluationType} from "@/lib/enums"
+
+export const VARIANT_COLORS = [
+ "#297F87", // "#722ed1",
+ "#F6D167", //"#13c2c2",
+]
+
+const useStyles = createUseStyles({
+ root: {
+ display: "flex",
+ gap: "1rem",
+ outline: "none",
+ },
+ evaluation: {
+ flex: 1,
+ display: "flex",
+ flexDirection: "column",
+ padding: "1rem",
+ "& .ant-divider": {
+ margin: "2rem 0 1.5rem 0",
+ },
+ "& h5.ant-typography": {
+ margin: 0,
+ marginBottom: "1rem",
+ },
+ gap: "1rem",
+ },
+ heading: {
+ width: "100%",
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ gap: "0.75rem",
+ "& .ant-typography": {
+ margin: 0,
+ fontWeight: 400,
+ },
+ },
+ headingDivider: {
+ position: "relative",
+ },
+ helpIcon: {
+ position: "absolute",
+ right: 0,
+ top: 42,
+ fontSize: 16,
+ },
+ instructions: {
+ paddingInlineStart: 0,
+ "& code": {
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
+ padding: "0.1rem 0.3rem",
+ borderRadius: 3,
+ },
+ "& li": {
+ marginBottom: "0.5rem",
+ },
+ },
+ note: {
+ marginTop: "1.25rem",
+ marginBottom: "-1rem",
+ whiteSpace: "pre-line",
+ display: "flex",
+ alignItems: "flex-start",
+
+ "& .anticon": {
+ marginTop: 4,
+ },
+ },
+ chatInputsCon: {
+ marginTop: "0.5rem",
+ },
+ correctAnswerCon: {
+ marginBottom: "0.5rem",
+ },
+ toolBar: {
+ display: "flex",
+ alignItems: "center",
+ gap: "0.5rem",
+ justifyContent: "flex-end",
+ "& .anticon": {
+ fontSize: 18,
+ cursor: "pointer",
+ },
+ },
+ sideBar: {
+ marginTop: "1rem",
+ display: "flex",
+ flexDirection: "column",
+ gap: "2rem",
+ border: "1px solid #d9d9d9",
+ borderRadius: 6,
+ padding: "1rem",
+ alignSelf: "flex-start",
+ "&>h4.ant-typography": {
+ margin: 0,
+ },
+ },
+})
+
+interface Props {
+ variants: Variant[]
+ evaluationScenarios: ABTestingEvaluationTableRow[]
+ onRun: (id: string) => void
+ onVote: (id: string, vote: string | number | null) => void
+ onInputChange: Function
+ updateEvaluationScenarioData: (id: string, data: Partial) => void
+ evaluation: Evaluation
+}
+
+const EvaluationCardView: React.FC = ({
+ variants,
+ evaluationScenarios,
+ onRun,
+ onVote,
+ onInputChange,
+ updateEvaluationScenarioData,
+ evaluation,
+}) => {
+ const classes = useStyles()
+ const {token} = theme.useToken()
+ const [scenarioId, setScenarioId] = useQueryParam(
+ "evaluationScenario",
+ evaluationScenarios[0]?.id || "",
+ )
+ const [instructionsShown, setInstructionsShown] = useLocalStorage(
+ "evalInstructionsShown",
+ false,
+ )
+ const {scenario, scenarioIndex} = useMemo(() => {
+ const scenarioIndex = evaluationScenarios.findIndex(
+ (scenario) => scenario.id === scenarioId,
+ )
+ return {scenario: evaluationScenarios[scenarioIndex], scenarioIndex}
+ }, [scenarioId, evaluationScenarios])
+
+ const rootRef = useRef(null)
+ const opened = useRef(false)
+ const callbacks = useRef({
+ onVote,
+ onRun,
+ onInputChange,
+ })
+ const isChat = !!evaluation.testset.testsetChatColumn
+ const testsetRow = evaluation.testset.csvdata[scenarioIndex]
+ const isAbTesting = evaluation.evaluationType === EvaluationType.human_a_b_testing
+
+ const loadPrevious = () => {
+ if (scenarioIndex === 0) return
+ setScenarioId(evaluationScenarios[scenarioIndex - 1].id)
+ }
+
+ const loadNext = () => {
+ if (scenarioIndex === evaluationScenarios.length - 1) return
+ setScenarioId(evaluationScenarios[scenarioIndex + 1].id)
+ }
+
+ const showInstructions = useCallback(() => {
+ if (opened.current) return
+
+ opened.current = true
+ AlertPopup({
+ title: "Instructions",
+ type: "info",
+ message: (
+
+
+ Use the buttons Next and Prev or the arrow keys{" "}
+ {`Left (<)`} and {`Right (>)`} to navigate between
+ evaluations.
+
+
+ Click the Run {" "}
+ button on
+ right or press {`Enter (↵)`} key to generate the variants'
+ outputs.
+
+ {isAbTesting && (
+
+ Vote by either clicking the evaluation buttons at the right
+ sidebar or pressing the key a for 1st Variant,{" "}
+ b for 2nd Variant and x if both are bad.
+
+ )}
+
+ Pin an evaluation to come back later by clicking the Pin {" "}
+ button on the right.
+
+
+ Add a note to an evaluation from the Additional Notes input section{" "}
+ in the right sidebar.
+
+
+ ),
+ okText: "Ok",
+ cancelText: null,
+ width: 500,
+ onCancel: () => (opened.current = false),
+ onOk: () => (opened.current = false),
+ })
+ }, [])
+
+ const depouncedUpdateEvaluationScenario = useCallback(
+ debounce((data: Partial) => {
+ updateEvaluationScenarioData(scenarioId, data)
+ }, 800),
+ [scenarioId],
+ )
+
+ const onChatChange = (chat: ChatMessage[]) => {
+ const stringified = JSON.stringify(chat)
+ testsetRow[evaluation.testset.testsetChatColumn] = stringified
+
+ depouncedUpdateEvaluationScenario({
+ inputs: [
+ {input_name: "chat", input_value: stringified},
+ ...scenario.inputs.filter((ip) => ip.input_name !== "chat"),
+ ],
+ [evaluation.testset.testsetChatColumn]: stringified,
+ })
+ }
+
+ //hack to always get the latest callbacks using ref
+ useEffect(() => {
+ callbacks.current = {onVote, onRun, onInputChange}
+ }, [onVote, onRun, onInputChange])
+
+ // focus the root element on mount
+ useEffect(() => {
+ if (rootRef.current) {
+ rootRef.current.focus()
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!instructionsShown) {
+ showInstructions()
+ setInstructionsShown(true)
+ }
+ }, [instructionsShown])
+
+ useEffect(() => {
+ if (typeof window === "undefined") return () => {}
+
+ const listener = (e: KeyboardEvent) => {
+ if (document.activeElement !== rootRef.current) return
+ if (e.key === "ArrowLeft") loadPrevious()
+ else if (e.key === "ArrowRight") loadNext()
+ else if (e.key === "Enter") callbacks.current.onRun(scenarioId)
+
+ if (isAbTesting) {
+ if (e.key === "a") callbacks.current.onVote(scenarioId, variants[0].variantId)
+ else if (e.key === "b") callbacks.current.onVote(scenarioId, variants[1].variantId)
+ else if (e.key === "x") callbacks.current.onVote(scenarioId, "0")
+ }
+ }
+
+ document.addEventListener("keydown", listener)
+ return () => document.removeEventListener("keydown", listener)
+ }, [scenarioIndex])
+
+ useEffect(() => {
+ if (scenario) {
+ const chatStr = scenario?.inputs.find((ip) => ip.input_name === "chat")?.input_value
+ if (chatStr) testsetRow[evaluation.testset.testsetChatColumn] = chatStr
+ }
+ }, [scenario])
+
+ const correctAnswer = useMemo(() => {
+ if (scenario?.correctAnswer) return scenario.correctAnswer
+ let res = testsetRow?.correct_answer
+ if (isChat) res = safeParse(res)?.content
+ return res || ""
+ }, [testsetRow?.correct_answer, scenario?.correctAnswer])
+
+ const chat = useMemo(() => {
+ const fromInput = scenario?.inputs.find((ip) => ip.input_name === "chat")?.input_value
+ if (!isChat) return []
+
+ return testsetRowToChatMessages(
+ fromInput
+ ? {chat: fromInput, correct_answer: testsetRow?.correct_answer}
+ : testsetRow || {},
+ false,
+ )
+ }, [scenarioIndex])
+
+ return (
+
+ {scenario ? (
+ <>
+
+
+ }
+ disabled={scenarioIndex === 0}
+ onClick={loadPrevious}
+ >
+ Prev
+
+
+ Evaluation: {scenarioIndex + 1}/{evaluationScenarios.length}
+
+
+
+ Next
+
+
+
+
+
+ {isChat ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {scenario.isPinned ? (
+
+
+ updateEvaluationScenarioData(scenarioId, {
+ isPinned: false,
+ })
+ }
+ />
+
+ ) : (
+
+
+ updateEvaluationScenarioData(scenarioId, {
+ isPinned: true,
+ })
+ }
+ />
+
+ )}
+
+ onRun(scenarioId)}
+ />
+
+
+
+
+ {!isAbTesting && (
+
+ Model Response
+
+ )}
+
+
+
+
+
+
+ Submit your feedback
+ {scenario.outputs.length > 0 &&
+ scenario.outputs.every((item) => !!item.variant_output) && (
+
+
+ {isAbTesting
+ ? "Which response is better?"
+ : "Rate the response"}
+
+ {isAbTesting ? (
+ onVote(scenarioId, vote)}
+ loading={scenario.vote === "loading"}
+ vertical
+ key={scenarioId}
+ />
+ ) : (
+ onVote(scenarioId, val[0].score)}
+ loading={scenario.score === "loading"}
+ showVariantName={false}
+ key={scenarioId}
+ />
+ )}
+
+ )}
+
+
+ Expected Answer
+
+ depouncedUpdateEvaluationScenario({
+ correctAnswer: e.target.value,
+ })
+ }
+ />
+
+
+
+ Additional Notes
+
+ depouncedUpdateEvaluationScenario({note: e.target.value})
+ }
+ />
+
+
+ >
+ ) : (
+
+ )}
+
+ )
+}
+
+export default EvaluationCardView
diff --git a/agenta-web/src/components/Evaluations/EvaluationErrorModal.tsx b/agenta-web/src/components/Evaluations/EvaluationErrorModal.tsx
index 15890523c5..78b9442b5a 100644
--- a/agenta-web/src/components/Evaluations/EvaluationErrorModal.tsx
+++ b/agenta-web/src/components/Evaluations/EvaluationErrorModal.tsx
@@ -1,5 +1,14 @@
import React from "react"
import {Modal, Button} from "antd"
+import {createUseStyles} from "react-jss"
+
+const useStyles = createUseStyles({
+ container: {
+ display: "flex",
+ justifyContent: "flex-end",
+ gap: 10,
+ },
+})
interface Props {
isModalOpen: boolean
@@ -16,6 +25,7 @@ const EvaluationErrorModal: React.FC = ({
btnText,
onClose,
}) => {
+ const classes = useStyles()
const handleCloseModal = () => onClose()
const handleCTAClick = () => {
@@ -24,13 +34,24 @@ const EvaluationErrorModal: React.FC = ({
}
return (
-
+
{message}
-
-
+
+
Ok
-
+
{btnText}
diff --git a/agenta-web/src/components/Evaluations/Evaluations.tsx b/agenta-web/src/components/Evaluations/Evaluations.tsx
index b155471bf1..5ffd16f94b 100644
--- a/agenta-web/src/components/Evaluations/Evaluations.tsx
+++ b/agenta-web/src/components/Evaluations/Evaluations.tsx
@@ -7,42 +7,143 @@ import {
Radio,
RadioChangeEvent,
Row,
- Tag,
- Slider,
+ Typography,
+ Select,
message,
+ ModalProps,
+ Tooltip,
} from "antd"
-import {DownOutlined} from "@ant-design/icons"
+import {DownOutlined, PlusOutlined, EditFilled} from "@ant-design/icons"
import {
+ createNewEvaluation,
fetchVariants,
- getVariantParametersFromOpenAPI,
useLoadTestsetsList,
+ fetchCustomEvaluations,
} from "@/lib/services/api"
-import {getOpenAIKey} from "@/lib/helpers/utils"
+import {dynamicComponent, getOpenAIKey, isDemo} from "@/lib/helpers/utils"
import {useRouter} from "next/router"
-import {Variant, Parameter} from "@/lib/Types"
-import EvaluationsList from "./EvaluationsList"
-import {EvaluationFlow, EvaluationType} from "@/lib/enums"
+import {Variant, Parameter, GenericObject, SingleCustomEvaluation} from "@/lib/Types"
+import {EvaluationType} from "@/lib/enums"
import {EvaluationTypeLabels} from "@/lib/helpers/utils"
-import {Typography} from "antd"
import EvaluationErrorModal from "./EvaluationErrorModal"
import {getAllVariantParameters} from "@/lib/helpers/variantHelper"
import Image from "next/image"
import abTesting from "@/media/testing.png"
+import singleModel from "@/media/score.png"
import exactMatch from "@/media/target.png"
import similarity from "@/media/transparency.png"
+import regexIcon from "@/media/programming.png"
+import webhookIcon from "@/media/link.png"
import ai from "@/media/artificial-intelligence.png"
+import codeIcon from "@/media/browser.png"
import {useAppTheme} from "../Layout/ThemeContextProvider"
+import {createUseStyles} from "react-jss"
+import AutomaticEvaluationResult from "./AutomaticEvaluationResult"
+import HumanEvaluationResult from "./HumanEvaluationResult"
+import {getErrorMessage} from "@/lib/helpers/errorHandler"
+
+type StyleProps = {
+ themeMode: "dark" | "light"
+}
+
+const useStyles = createUseStyles({
+ evaluationContainer: {
+ border: "1px solid lightgrey",
+ padding: "20px",
+ borderRadius: "14px",
+ marginBottom: 50,
+ },
+ evaluationImg: ({themeMode}: StyleProps) => ({
+ width: 24,
+ height: 24,
+ marginRight: "8px",
+ filter: themeMode === "dark" ? "invert(1)" : "none",
+ }),
+ createCustomEvalBtn: {
+ color: "#fff !important",
+ backgroundColor: "#0fbf0f",
+ marginRight: "20px",
+ borderColor: "#0fbf0f !important",
+ },
+ evaluationType: {
+ display: "flex",
+ alignItems: "center",
+ },
+ dropdownStyles: {
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ width: "100%",
+ },
+ dropdownBtn: {
+ marginRight: 10,
+ marginTop: 40,
+ width: "100%",
+ },
+ optionSelected: {
+ border: "1px solid #1668dc",
+ "& .ant-select-selection-item": {
+ color: "#1668dc !important",
+ },
+ },
+ radioGroup: {
+ width: "100%",
+ },
+ radioBtn: {
+ display: "block",
+ marginBottom: "10px",
+ },
+ selectGroup: {
+ width: "100%",
+ display: "block",
+ "& .ant-select-selector": {
+ borderRadius: 0,
+ },
+ "& .ant-select-selection-item": {
+ marginLeft: 34,
+ },
+ },
+ customCodeSelectContainer: {
+ position: "relative",
+ },
+ customCodeIcon: {
+ position: "absolute",
+ left: 16,
+ top: 4.5,
+ pointerEvents: "none",
+ },
+ thresholdStyles: {
+ paddingLeft: 10,
+ paddingRight: 10,
+ },
+ variantDropdown: {
+ marginRight: 10,
+ width: "100%",
+ },
+ newCodeEval: {
+ display: "flex",
+ alignItems: "center",
+ gap: 8,
+ color: "#1668dc",
+ },
+ newCodeEvalList: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+})
+const {Title} = Typography
export default function Evaluations() {
- const {Text, Title} = Typography
const router = useRouter()
const {appTheme} = useAppTheme()
const [areAppVariantsLoading, setAppVariantsLoading] = useState(false)
const [isError, setIsError] = useState(false)
const [variants, setVariants] = useState([])
+ const classes = useStyles({themeMode: appTheme} as StyleProps)
+ const {Option} = Select
- const [columnsCount, setColumnsCount] = useState(2)
const [selectedTestset, setSelectedTestset] = useState<{
_id?: string
name: string
@@ -57,23 +158,31 @@ export default function Evaluations() {
const [selectedEvaluationType, setSelectedEvaluationType] = useState(
"Select an evaluation type",
)
+ const [selectedCustomEvaluationID, setSelectedCustomEvaluationID] = useState("")
- const appName = router.query.app_name?.toString() || ""
+ const appId = router.query.app_id?.toString() || ""
- const {testsets, isTestsetsLoading, isTestsetsLoadingError} = useLoadTestsetsList(appName)
+ const {testsets, isTestsetsLoadingError} = useLoadTestsetsList(appId)
const [variantsInputs, setVariantsInputs] = useState>({})
- const [sliderValue, setSliderValue] = useState(0.3)
-
const [error, setError] = useState({message: "", btnText: "", endpoint: ""})
const [llmAppPromptTemplate, setLLMAppPromptTemplate] = useState("")
+ const [customCodeEvaluationList, setCustomCodeEvaluationList] =
+ useState()
+
+ const [shareModalOpen, setShareModalOpen] = useState(false)
+
+ const ShareEvaluationModal = dynamicComponent(
+ "Evaluations/ShareEvaluationModal",
+ )
+
useEffect(() => {
const fetchData = async () => {
try {
- const backendVariants = await fetchVariants(appName)
+ const backendVariants = await fetchVariants(appId)
if (backendVariants.length > 0) {
setVariants(backendVariants)
@@ -87,7 +196,7 @@ export default function Evaluations() {
}
fetchData()
- }, [appName])
+ }, [appId])
useEffect(() => {
if (variants.length > 0) {
@@ -95,9 +204,10 @@ export default function Evaluations() {
try {
// Map the variants to an array of promises
const promises = variants.map((variant) =>
- getAllVariantParameters(appName, variant).then(({inputs}) => ({
+ getAllVariantParameters(appId, variant).then((data) => ({
variantName: variant.variantName,
- inputs: inputs.map((inputParam: Parameter) => inputParam.name),
+ inputs:
+ data?.inputs.map((inputParam: Parameter) => inputParam.name) || [],
})),
)
@@ -106,7 +216,7 @@ export default function Evaluations() {
// Reduce the results into the desired newVariantsInputs object structure
const newVariantsInputs: Record = results.reduce(
- (acc, result) => {
+ (acc: GenericObject, result) => {
acc[result.variantName] = result.inputs
return acc
},
@@ -114,14 +224,14 @@ export default function Evaluations() {
)
setVariantsInputs(newVariantsInputs)
- } catch (e) {
- setIsError("Failed to fetch some variants parameters. Error: " + e.message)
+ } catch (e: any) {
+ setIsError("Failed to fetch some variants parameters. Error: " + e?.message)
}
}
fetchAndSetSchema()
}
- }, [appName, variants])
+ }, [appId, variants])
useEffect(() => {
if (!isTestsetsLoadingError && testsets) {
@@ -129,56 +239,6 @@ export default function Evaluations() {
}
}, [testsets, isTestsetsLoadingError])
- // TODO: move to api.ts
- const createNewEvaluation = async (
- evaluationType: string,
- evaluationTypeSettings: any,
- inputs: string[],
- llmAppPromptTemplate?: string,
- ) => {
- const postData = async (url = "", data = {}) => {
- const response = await fetch(url, {
- method: "POST",
- cache: "no-cache",
- credentials: "same-origin",
- headers: {
- "Content-Type": "application/json",
- },
- redirect: "follow",
- referrerPolicy: "no-referrer",
- body: JSON.stringify(data),
- })
-
- if (!response.ok) {
- throw new Error((await response.json())?.detail ?? "Failed to create evaluation")
- }
-
- return response.json()
- }
-
- const data = {
- variants: selectedVariants.map((variant) => variant.variantName), // TODO: Change to variant id
- app_name: appName,
- inputs: inputs,
- evaluation_type: evaluationType,
- evaluation_type_settings: evaluationTypeSettings,
- llm_app_prompt_template: llmAppPromptTemplate,
- testset: {
- _id: selectedTestset._id,
- name: selectedTestset.name,
- },
- status: EvaluationFlow.EVALUATION_INITIALIZED,
- }
-
- return postData(`${process.env.NEXT_PUBLIC_AGENTA_API_URL}/api/evaluations/`, data)
- .then((data) => {
- return data.id
- })
- .catch((err) => {
- setError({message: err.message, btnText: "Go to Test sets", endpoint: "testsets"})
- })
- }
-
const onTestsetSelect = (selectedTestsetIndexInTestsetsList: number) => {
setSelectedTestset(testsetsList[selectedTestsetIndexInTestsetsList])
}
@@ -186,7 +246,11 @@ export default function Evaluations() {
const getTestsetDropdownMenu = (): MenuProps => {
const items: MenuProps["items"] = testsetsList.map((testset, index) => {
return {
- label: testset.name,
+ label: (
+ <>
+