diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9580a3ba3..f52e411cd6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,13 +8,13 @@ body: label: Self Checks description: "To make sure we get to you in time, please check the following :)" options: + - label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542). + required: true - label: This is only for bug report, if you would like to ask a question, please head to [Discussions](https://github.com/langgenius/dify/discussions/categories/general). required: true - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. required: true - - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). - required: true - - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + - label: I confirm that I am using English to submit this report, otherwise it will be closed. required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true @@ -42,20 +42,22 @@ body: attributes: label: Steps to reproduce description: We highly suggest including screenshots and a bug report log. Please use the right markdown syntax for code blocks. - placeholder: Having detailed steps helps us reproduce the bug. + placeholder: Having detailed steps helps us reproduce the bug. If you have logs, please use fenced code blocks (triple backticks ```) to format them. validations: required: true - type: textarea attributes: label: ✔️ Expected Behavior - placeholder: What were you expecting? + description: Describe what you expected to happen. + placeholder: What were you expecting? Please do not copy and paste the steps to reproduce here. validations: - required: false + required: true - type: textarea attributes: label: ❌ Actual Behavior - placeholder: What happened instead? + description: Describe what actually happened. + placeholder: What happened instead? Please do not copy and paste the steps to reproduce here. validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6877c382c4..c1666d24cf 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: false contact_links: + - name: "\U0001F4A1 Model Providers & Plugins" + url: "https://github.com/langgenius/dify-official-plugins/issues/new/choose" + about: Report issues with official plugins or model providers, you will need to provide the plugin version and other relevant details. + - name: "\U0001F4AC Documentation Issues" + url: "https://github.com/langgenius/dify-docs/issues/new" + about: Report issues with the documentation, such as typos, outdated information, or missing content. Please provide the specific section and details of the issue. - name: "\U0001F4E7 Discussions" url: https://github.com/langgenius/dify/discussions/categories/general - about: General discussions and request help from the community + about: General discussions and seek help from the community diff --git a/.github/ISSUE_TEMPLATE/document_issue.yml b/.github/ISSUE_TEMPLATE/document_issue.yml deleted file mode 100644 index 8fdbc0fb9a..0000000000 --- a/.github/ISSUE_TEMPLATE/document_issue.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "📚 Documentation Issue" -description: Report issues in our documentation -labels: - - documentation -body: - - type: checkboxes - attributes: - label: Self Checks - description: "To make sure we get to you in time, please check the following :)" - options: - - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. - required: true - - label: I confirm that I am using English to submit report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). - required: true - - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" - required: true - - label: "Please do not modify this template :) and fill in all the required fields." - required: true - - type: textarea - attributes: - label: Provide a description of requested docs changes - placeholder: Briefly describe which document needs to be corrected and why. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b1952c63a9..bd293e2442 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -8,11 +8,11 @@ body: label: Self Checks description: "To make sure we get to you in time, please check the following :)" options: - - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. + - label: I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542). required: true - - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). + - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. required: true - - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" + - label: I confirm that I am using English to submit this report, otherwise it will be closed. required: true - label: "Please do not modify this template :) and fill in all the required fields." required: true diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml deleted file mode 100644 index f9c2dfb7d2..0000000000 --- a/.github/ISSUE_TEMPLATE/translation_issue.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: "🌐 Localization/Translation issue" -description: Report incorrect translations. [please use English :)] -labels: - - translation -body: - - type: checkboxes - attributes: - label: Self Checks - description: "To make sure we get to you in time, please check the following :)" - options: - - label: I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones. - required: true - - label: I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). - required: true - - label: "[FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:)" - required: true - - label: "Please do not modify this template :) and fill in all the required fields." - required: true - - type: input - attributes: - label: Dify version - description: Hover over system tray icon or look at Settings - validations: - required: true - - type: input - attributes: - label: Utility with translation issue - placeholder: Some area - description: Please input here the utility with the translation issue - validations: - required: true - - type: input - attributes: - label: 🌐 Language affected - placeholder: "German" - validations: - required: true - - type: textarea - attributes: - label: ❌ Actual phrase(s) - placeholder: What is there? Please include a screenshot as that is extremely helpful. - validations: - required: true - - type: textarea - attributes: - label: ✔️ Expected phrase(s) - placeholder: What was expected? - validations: - required: true - - type: textarea - attributes: - label: ℹ Why is the current translation wrong - placeholder: Why do you feel this is incorrect? - validations: - required: true diff --git a/.github/actions/setup-uv/action.yml b/.github/actions/setup-uv/action.yml index a596be63f7..0499b44dba 100644 --- a/.github/actions/setup-uv/action.yml +++ b/.github/actions/setup-uv/action.yml @@ -8,7 +8,7 @@ inputs: uv-version: description: UV version to set up required: true - default: '0.6.14' + default: '~=0.7.11' uv-lockfile: description: Path to the UV lockfile to restore cache from required: true diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index f08befefb8..a5a5071fae 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -47,15 +47,17 @@ jobs: - name: Run Unit tests run: | uv run --project api bash dev/pytest/pytest_unit_tests.sh + + - name: Coverage Summary + run: | + set -x # Extract coverage percentage and create a summary TOTAL_COVERAGE=$(python -c 'import json; print(json.load(open("coverage.json"))["totals"]["percent_covered_display"])') # Create a detailed coverage summary echo "### Test Coverage Summary :test_tube:" >> $GITHUB_STEP_SUMMARY echo "Total Coverage: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - uv run --project api coverage report >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + uv run --project api coverage report --format=markdown >> $GITHUB_STEP_SUMMARY - name: Run dify config tests run: uv run --project api dev/pytest/pytest_config_tests.py @@ -83,9 +85,15 @@ jobs: compose-file: | docker/docker-compose.middleware.yaml services: | + db + redis sandbox ssrf_proxy + - name: setup test config + run: | + cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env + - name: Run Workflow run: uv run --project api bash dev/pytest/pytest_workflow.sh diff --git a/.github/workflows/deploy-rag-dev.yml b/.github/workflows/deploy-rag-dev.yml new file mode 100644 index 0000000000..86265aad6d --- /dev/null +++ b/.github/workflows/deploy-rag-dev.yml @@ -0,0 +1,28 @@ +name: Deploy RAG Dev + +permissions: + contents: read + +on: + workflow_run: + workflows: ["Build and Push API & Web"] + branches: + - "deploy/rag-dev" + types: + - completed + +jobs: + deploy: + runs-on: ubuntu-latest + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.head_branch == 'deploy/rag-dev' + steps: + - name: Deploy to server + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.RAG_SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + ${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }} diff --git a/.github/workflows/expose_service_ports.sh b/.github/workflows/expose_service_ports.sh index 10d95cb736..01772ccf9f 100755 --- a/.github/workflows/expose_service_ports.sh +++ b/.github/workflows/expose_service_ports.sh @@ -10,6 +10,7 @@ yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-com yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml +yq eval '.services.oceanbase.ports += ["2881:2881"]' -i docker/docker-compose.yaml yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml echo "Ports exposed for sandbox, weaviate, tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss" diff --git a/.github/workflows/vdb-tests.yml b/.github/workflows/vdb-tests.yml index c784817e72..912267094b 100644 --- a/.github/workflows/vdb-tests.yml +++ b/.github/workflows/vdb-tests.yml @@ -31,6 +31,13 @@ jobs: with: persist-credentials: false + - name: Free Disk Space + uses: endersonmenezes/free-disk-space@v2 + with: + remove_dotnet: true + remove_haskell: true + remove_tool_cache: true + - name: Setup UV and Python uses: ./.github/actions/setup-uv with: @@ -59,7 +66,7 @@ jobs: tidb tiflash - - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase) + - name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase) uses: hoverkraft-tech/compose-action@v2.0.2 with: compose-file: | @@ -75,8 +82,15 @@ jobs: pgvector chroma elasticsearch + oceanbase + + - name: setup test config + run: | + echo $(pwd) + ls -lah . + cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env - - name: Check TiDB Ready + - name: Check VDB Ready (TiDB) run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py - name: Test Vector Stores diff --git a/.gitignore b/.gitignore index 74a9ef63ef..dd4673a3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -179,6 +179,7 @@ docker/volumes/pgvecto_rs/data/* docker/volumes/couchbase/* docker/volumes/oceanbase/* docker/volumes/plugin_daemon/* +docker/volumes/matrixone/* !docker/volumes/oceanbase/init.d docker/nginx/conf.d/default.conf @@ -210,3 +211,7 @@ mise.toml # Next.js build output .next/ + +# AI Assistant +.roo/ +api/.env.backup diff --git a/README.md b/README.md index ca09adec08..2909e0e6cf 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ README in বাংলা

-Dify is an open-source LLM app development platform. Its intuitive interface combines agentic AI workflow, RAG pipeline, agent capabilities, model management, observability features, and more, allowing you to quickly move from prototype to production. +Dify is an open-source platform for developing LLM applications. Its intuitive interface combines agentic AI workflows, RAG pipelines, agent capabilities, model management, observability features, and more—allowing you to quickly move from prototype to production. ## Quick start @@ -65,7 +65,7 @@ Dify is an open-source LLM app development platform. Its intuitive interface com
-The easiest way to start the Dify server is through [docker compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: +The easiest way to start the Dify server is through [Docker Compose](docker/docker-compose.yaml). Before running Dify with the following commands, make sure that [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) are installed on your machine: ```bash cd dify @@ -205,6 +205,7 @@ If you'd like to configure a highly-available setup, there are community-contrib - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Using Terraform for Deployment @@ -226,6 +227,15 @@ Deploy Dify to AWS with [CDK](https://aws.amazon.com/cdk/) - [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Using Alibaba Cloud Computing Nest + +Quickly deploy Dify to Alibaba cloud with [Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Using Alibaba Cloud Data Management + +One-Click deploy Dify to Alibaba Cloud with [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contributing For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). @@ -252,8 +262,8 @@ At the same time, please consider supporting Dify by sharing it on social media ## Security disclosure -To protect your privacy, please avoid posting security issues on GitHub. Instead, send your questions to security@dify.ai and we will provide you with a more detailed answer. +To protect your privacy, please avoid posting security issues on GitHub. Instead, report issues to security@dify.ai, and our team will respond with detailed answer. ## License -This repository is available under the [Dify Open Source License](LICENSE), which is essentially Apache 2.0 with a few additional restrictions. +This repository is licensed under the [Dify Open Source License](LICENSE), based on Apache 2.0 with additional conditions. diff --git a/README_AR.md b/README_AR.md index df288fd33c..e959ca0f78 100644 --- a/README_AR.md +++ b/README_AR.md @@ -188,6 +188,7 @@ docker compose up -d - [رسم بياني Helm من قبل @magicsong](https://github.com/magicsong/ai-charts) - [ملف YAML من قبل @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [ملف YAML من قبل @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 جديد! ملفات YAML (تدعم Dify v1.6.0) بواسطة @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### استخدام Terraform للتوزيع @@ -209,6 +210,14 @@ docker compose up -d - [AWS CDK بواسطة @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### استخدام Alibaba Cloud للنشر + [بسرعة نشر Dify إلى سحابة علي بابا مع عش الحوسبة السحابية علي بابا](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### استخدام Alibaba Cloud Data Management للنشر + +انشر ​​Dify على علي بابا كلاود بنقرة واحدة باستخدام [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## المساهمة لأولئك الذين يرغبون في المساهمة، انظر إلى [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) لدينا. diff --git a/README_BN.md b/README_BN.md index 4a5b5f3928..29d7374ea5 100644 --- a/README_BN.md +++ b/README_BN.md @@ -204,6 +204,8 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 নতুন! YAML ফাইলসমূহ (Dify v1.6.0 সমর্থিত) তৈরি করেছেন @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) + #### টেরাফর্ম ব্যবহার করে ডিপ্লয় @@ -225,6 +227,15 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন - [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud ব্যবহার করে ডিপ্লয় + + [Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management ব্যবহার করে ডিপ্লয় + + [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contributing যারা কোড অবদান রাখতে চান, তাদের জন্য আমাদের [অবদান নির্দেশিকা] দেখুন (https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)। diff --git a/README_CN.md b/README_CN.md index ba7ee0006d..486a368c09 100644 --- a/README_CN.md +++ b/README_CN.md @@ -194,9 +194,9 @@ docker compose up -d 如果您需要自定义配置,请参考 [.env.example](docker/.env.example) 文件中的注释,并更新 `.env` 文件中对应的值。此外,您可能需要根据您的具体部署环境和需求对 `docker-compose.yaml` 文件本身进行调整,例如更改镜像版本、端口映射或卷挂载。完成任何更改后,请重新运行 `docker-compose up -d`。您可以在[此处](https://docs.dify.ai/getting-started/install-self-hosted/environments)找到可用环境变量的完整列表。 -#### 使用 Helm Chart 部署 +#### 使用 Helm Chart 或 Kubernetes 资源清单(YAML)部署 -使用 [Helm Chart](https://helm.sh/) 版本或者 YAML 文件,可以在 Kubernetes 上部署 Dify。 +使用 [Helm Chart](https://helm.sh/) 版本或者 Kubernetes 资源清单(YAML),可以在 Kubernetes 上部署 Dify。 - [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify) - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) @@ -204,6 +204,10 @@ docker compose up -d - [YAML 文件 by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML 文件 (支持 Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) + + + #### 使用 Terraform 部署 使用 [terraform](https://www.terraform.io/) 一键将 Dify 部署到云平台 @@ -221,6 +225,15 @@ docker compose up -d ##### AWS - [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### 使用 阿里云计算巢 部署 + +使用 [阿里云计算巢](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) 将 Dify 一键部署到 阿里云 + +#### 使用 阿里云数据管理DMS 部署 + +使用 [阿里云数据管理DMS](https://help.aliyun.com/zh/dms/dify-in-invitational-preview) 将 Dify 一键部署到 阿里云 + + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=langgenius/dify&type=Date)](https://star-history.com/#langgenius/dify&Date) diff --git a/README_DE.md b/README_DE.md index f6023a3935..fce52c34c2 100644 --- a/README_DE.md +++ b/README_DE.md @@ -203,6 +203,7 @@ Falls Sie eine hochverfügbare Konfiguration einrichten möchten, gibt es von de - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Terraform für die Bereitstellung verwenden @@ -221,6 +222,15 @@ Bereitstellung von Dify auf AWS mit [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Ein-Klick-Bereitstellung von Dify in der Alibaba Cloud mit [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contributing Falls Sie Code beitragen möchten, lesen Sie bitte unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). Gleichzeitig bitten wir Sie, Dify zu unterstützen, indem Sie es in den sozialen Medien teilen und auf Veranstaltungen und Konferenzen präsentieren. diff --git a/README_ES.md b/README_ES.md index 12f2ce8c11..6fd6dfcee8 100644 --- a/README_ES.md +++ b/README_ES.md @@ -203,6 +203,7 @@ Si desea configurar una configuración de alta disponibilidad, la comunidad prop - [Gráfico Helm por @magicsong](https://github.com/magicsong/ai-charts) - [Ficheros YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [Ficheros YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 ¡NUEVO! Archivos YAML (compatible con Dify v1.6.0) por @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Uso de Terraform para el despliegue @@ -221,6 +222,15 @@ Despliegue Dify en AWS usando [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Despliega Dify en Alibaba Cloud con un solo clic con [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contribuir Para aquellos que deseen contribuir con código, consulten nuestra [Guía de contribución](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_FR.md b/README_FR.md index b106615b31..b2209fb495 100644 --- a/README_FR.md +++ b/README_FR.md @@ -201,6 +201,7 @@ Si vous souhaitez configurer une configuration haute disponibilité, la communau - [Helm Chart par @magicsong](https://github.com/magicsong/ai-charts) - [Fichier YAML par @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [Fichier YAML par @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NOUVEAU ! Fichiers YAML (compatible avec Dify v1.6.0) par @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Utilisation de Terraform pour le déploiement @@ -219,6 +220,15 @@ Déployez Dify sur AWS en utilisant [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK par @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Déployez Dify en un clic sur Alibaba Cloud avec [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contribuer Pour ceux qui souhaitent contribuer du code, consultez notre [Guide de contribution](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_JA.md b/README_JA.md index 26703f3958..c658225f90 100644 --- a/README_JA.md +++ b/README_JA.md @@ -155,7 +155,7 @@ DifyはオープンソースのLLMアプリケーション開発プラットフ [こちら](https://dify.ai)のDify Cloudサービスを利用して、セットアップ不要で試すことができます。サンドボックスプランには、200回のGPT-4呼び出しが無料で含まれています。 - **Dify Community Editionのセルフホスティング
** -この[スタートガイド](#quick-start)を使用して、ローカル環境でDifyを簡単に実行できます。 +この[スタートガイド](#クイックスタート)を使用して、ローカル環境でDifyを簡単に実行できます。 詳しくは[ドキュメント](https://docs.dify.ai)をご覧ください。 - **企業/組織向けのDify
** @@ -202,6 +202,7 @@ docker compose up -d - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 新着!YAML ファイル(Dify v1.6.0 対応)by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Terraformを使用したデプロイ @@ -220,6 +221,13 @@ docker compose up -d ##### AWS - [@KevinZhaoによるAWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management +[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) を利用して、DifyをAlibaba Cloudへワンクリックでデプロイできます + + ## 貢献 コードに貢献したい方は、[Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)を参照してください。 diff --git a/README_KL.md b/README_KL.md index ea91baa5aa..bfafcc7407 100644 --- a/README_KL.md +++ b/README_KL.md @@ -201,6 +201,7 @@ If you'd like to configure a highly-available setup, there are community-contrib - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Terraform atorlugu pilersitsineq @@ -219,6 +220,15 @@ wa'logh nIqHom neH ghun deployment toy'wI' [CDK](https://aws.amazon.com/cdk/) lo ##### AWS - [AWS CDK qachlot @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contributing For those who'd like to contribute code, see our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_KR.md b/README_KR.md index 89301e8b2c..282117e776 100644 --- a/README_KR.md +++ b/README_KR.md @@ -195,6 +195,7 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 - [Helm Chart by @magicsong](https://github.com/magicsong/ai-charts) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Terraform을 사용한 배포 @@ -213,6 +214,15 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했 ##### AWS - [KevinZhao의 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/)를 통해 원클릭으로 Dify를 Alibaba Cloud에 배포할 수 있습니다 + + ## 기여 코드에 기여하고 싶은 분들은 [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요. diff --git a/README_PT.md b/README_PT.md index 157772d528..576f6b48f7 100644 --- a/README_PT.md +++ b/README_PT.md @@ -200,6 +200,7 @@ Se deseja configurar uma instalação de alta disponibilidade, há [Helm Charts] - [Helm Chart de @magicsong](https://github.com/magicsong/ai-charts) - [Arquivo YAML por @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [Arquivo YAML por @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NOVO! Arquivos YAML (Compatível com Dify v1.6.0) por @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Usando o Terraform para Implantação @@ -218,6 +219,15 @@ Implante o Dify na AWS usando [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK por @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Implante o Dify na Alibaba Cloud com um clique usando o [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Contribuindo Para aqueles que desejam contribuir com código, veja nosso [Guia de Contribuição](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md). diff --git a/README_SI.md b/README_SI.md index 14de1ea792..7ded001d86 100644 --- a/README_SI.md +++ b/README_SI.md @@ -201,6 +201,7 @@ Star Dify on GitHub and be instantly notified of new releases. - [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [YAML file by @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [YAML file by @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML files (Supports Dify v1.6.0) by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Uporaba Terraform za uvajanje @@ -219,6 +220,15 @@ Uvedite Dify v AWS z uporabo [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK by @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Z enim klikom namestite Dify na Alibaba Cloud z [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Prispevam Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkrati vas prosimo, da podprete Dify tako, da ga delite na družbenih medijih ter na dogodkih in konferencah. diff --git a/README_TR.md b/README_TR.md index 563a05af3c..6e94e54fa0 100644 --- a/README_TR.md +++ b/README_TR.md @@ -194,6 +194,7 @@ Yüksek kullanılabilirliğe sahip bir kurulum yapılandırmak isterseniz, Dify' - [@BorisPolonsky tarafından Helm Chart](https://github.com/BorisPolonsky/dify-helm) - [@Winson-030 tarafından YAML dosyası](https://github.com/Winson-030/dify-kubernetes) - [@wyy-holding tarafından YAML dosyası](https://github.com/wyy-holding/dify-k8s) +- [🚀 YENİ! YAML dosyaları (Dify v1.6.0 destekli) @Zhoneym tarafından](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Dağıtım için Terraform Kullanımı @@ -212,6 +213,15 @@ Dify'ı bulut platformuna tek tıklamayla dağıtın [terraform](https://www.ter ##### AWS - [AWS CDK tarafından @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +[Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) kullanarak Dify'ı tek tıkla Alibaba Cloud'a dağıtın + + ## Katkıda Bulunma Kod katkısında bulunmak isteyenler için [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakabilirsiniz. diff --git a/README_TW.md b/README_TW.md index f4a76ac109..6e3e22b5c1 100644 --- a/README_TW.md +++ b/README_TW.md @@ -197,12 +197,13 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify 如果您需要自定義配置,請參考我們的 [.env.example](docker/.env.example) 文件中的註釋,並在您的 `.env` 文件中更新相應的值。此外,根據您特定的部署環境和需求,您可能需要調整 `docker-compose.yaml` 文件本身,例如更改映像版本、端口映射或卷掛載。進行任何更改後,請重新運行 `docker-compose up -d`。您可以在[這裡](https://docs.dify.ai/getting-started/install-self-hosted/environments)找到可用環境變數的完整列表。 -如果您想配置高可用性設置,社區貢獻的 [Helm Charts](https://helm.sh/) 和 YAML 文件允許在 Kubernetes 上部署 Dify。 +如果您想配置高可用性設置,社區貢獻的 [Helm Charts](https://helm.sh/) 和 Kubernetes 資源清單(YAML)允許在 Kubernetes 上部署 Dify。 - [由 @LeoQuote 提供的 Helm Chart](https://github.com/douban/charts/tree/master/charts/dify) - [由 @BorisPolonsky 提供的 Helm Chart](https://github.com/BorisPolonsky/dify-helm) - [由 @Winson-030 提供的 YAML 文件](https://github.com/Winson-030/dify-kubernetes) - [由 @wyy-holding 提供的 YAML 文件](https://github.com/wyy-holding/dify-k8s) +- [🚀 NEW! YAML 檔案(支援 Dify v1.6.0)by @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) ### 使用 Terraform 進行部署 @@ -224,6 +225,15 @@ Dify 的所有功能都提供相應的 API,因此您可以輕鬆地將 Dify - [由 @KevinZhao 提供的 AWS CDK](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) +#### 使用 阿里云计算巢進行部署 + +[阿里云](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### 使用 阿里雲數據管理DMS 進行部署 + +透過 [阿里雲數據管理DMS](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/),一鍵將 Dify 部署至阿里雲 + + ## 貢獻 對於想要貢獻程式碼的開發者,請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。 diff --git a/README_VI.md b/README_VI.md index 4e1e05cbf3..51314e6de5 100644 --- a/README_VI.md +++ b/README_VI.md @@ -196,6 +196,7 @@ Nếu bạn muốn cấu hình một cài đặt có độ sẵn sàng cao, có - [Helm Chart bởi @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm) - [Tệp YAML bởi @Winson-030](https://github.com/Winson-030/dify-kubernetes) - [Tệp YAML bởi @wyy-holding](https://github.com/wyy-holding/dify-k8s) +- [🚀 MỚI! Tệp YAML (Hỗ trợ Dify v1.6.0) bởi @Zhoneym](https://github.com/Zhoneym/DifyAI-Kubernetes) #### Sử dụng Terraform để Triển khai @@ -214,6 +215,16 @@ Triển khai Dify trên AWS bằng [CDK](https://aws.amazon.com/cdk/) ##### AWS - [AWS CDK bởi @KevinZhao](https://github.com/aws-samples/solution-for-deploying-dify-on-aws) + +#### Alibaba Cloud + +[Alibaba Cloud Computing Nest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=Dify%E7%A4%BE%E5%8C%BA%E7%89%88) + +#### Alibaba Cloud Data Management + +Triển khai Dify lên Alibaba Cloud chỉ với một cú nhấp chuột bằng [Alibaba Cloud Data Management](https://www.alibabacloud.com/help/en/dms/dify-in-invitational-preview/) + + ## Đóng góp Đối với những người muốn đóng góp mã, xem [Hướng dẫn Đóng góp](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) của chúng tôi. diff --git a/api/.env.example b/api/.env.example index 7878308588..eab017a624 100644 --- a/api/.env.example +++ b/api/.env.example @@ -17,6 +17,11 @@ APP_WEB_URL=http://127.0.0.1:3000 # Files URL FILES_URL=http://127.0.0.1:5001 +# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network. +# Set this to the internal Docker service URL for proper plugin file access. +# Example: INTERNAL_FILES_URL=http://api:5001 +INTERNAL_FILES_URL=http://127.0.0.1:5001 + # The time in seconds after the signature is rejected FILES_ACCESS_TIMEOUT=300 @@ -137,7 +142,7 @@ WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* # Vector database configuration -# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss, tablestore +# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase, opengauss, tablestore, matrixone VECTOR_STORE=weaviate # Weaviate configuration @@ -294,6 +299,13 @@ VIKINGDB_SCHEMA=http VIKINGDB_CONNECTION_TIMEOUT=30 VIKINGDB_SOCKET_TIMEOUT=30 +# Matrixone configration +MATRIXONE_HOST=127.0.0.1 +MATRIXONE_PORT=6001 +MATRIXONE_USER=dump +MATRIXONE_PASSWORD=111 +MATRIXONE_DATABASE=dify + # Lindorm configuration LINDORM_URL=http://ld-*******************-proxy-search-pub.lindorm.aliyuncs.com:30070 LINDORM_USERNAME=admin @@ -332,9 +344,11 @@ PROMPT_GENERATION_MAX_TOKENS=512 CODE_GENERATION_MAX_TOKENS=1024 PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false -# Mail configuration, support: resend, smtp +# Mail configuration, support: resend, smtp, sendgrid MAIL_TYPE= +# If using SendGrid, use the 'from' field for authentication if necessary. MAIL_DEFAULT_SEND_FROM=no-reply +# resend configuration RESEND_API_KEY= RESEND_API_URL=https://api.resend.com # smtp configuration @@ -344,7 +358,8 @@ SMTP_USERNAME=123 SMTP_PASSWORD=abc SMTP_USE_TLS=true SMTP_OPPORTUNISTIC_TLS=false - +# Sendgid configuration +SENDGRID_API_KEY= # Sentry configuration SENTRY_DSN= @@ -434,6 +449,19 @@ MAX_VARIABLE_SIZE=204800 # hybrid: Save new data to object storage, read from both object storage and RDBMS WORKFLOW_NODE_EXECUTION_STORAGE=rdbms +# Repository configuration +# Core workflow execution repository implementation +CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository + +# Core workflow node execution repository implementation +CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository + +# API workflow node execution repository implementation +API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository + +# API workflow run repository implementation +API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository + # App configuration APP_MAX_EXECUTION_TIME=1200 APP_MAX_ACTIVE_REQUESTS=0 diff --git a/api/.ruff.toml b/api/.ruff.toml index 41a24abad9..0169613bf8 100644 --- a/api/.ruff.toml +++ b/api/.ruff.toml @@ -1,6 +1,4 @@ -exclude = [ - "migrations/*", -] +exclude = ["migrations/*"] line-length = 120 [format] @@ -9,14 +7,14 @@ quote-style = "double" [lint] preview = false select = [ - "B", # flake8-bugbear rules - "C4", # flake8-comprehensions - "E", # pycodestyle E rules - "F", # pyflakes rules - "FURB", # refurb rules - "I", # isort rules - "N", # pep8-naming - "PT", # flake8-pytest-style rules + "B", # flake8-bugbear rules + "C4", # flake8-comprehensions + "E", # pycodestyle E rules + "F", # pyflakes rules + "FURB", # refurb rules + "I", # isort rules + "N", # pep8-naming + "PT", # flake8-pytest-style rules "PLC0208", # iteration-over-set "PLC0414", # useless-import-alias "PLE0604", # invalid-all-object @@ -24,58 +22,60 @@ select = [ "PLR0402", # manual-from-import "PLR1711", # useless-return "PLR1714", # repeated-equality-comparison - "RUF013", # implicit-optional - "RUF019", # unnecessary-key-check - "RUF100", # unused-noqa - "RUF101", # redirected-noqa - "RUF200", # invalid-pyproject-toml - "RUF022", # unsorted-dunder-all - "S506", # unsafe-yaml-load - "SIM", # flake8-simplify rules - "TRY400", # error-instead-of-exception - "TRY401", # verbose-log-message - "UP", # pyupgrade rules - "W191", # tab-indentation - "W605", # invalid-escape-sequence + "RUF013", # implicit-optional + "RUF019", # unnecessary-key-check + "RUF100", # unused-noqa + "RUF101", # redirected-noqa + "RUF200", # invalid-pyproject-toml + "RUF022", # unsorted-dunder-all + "S506", # unsafe-yaml-load + "SIM", # flake8-simplify rules + "TRY400", # error-instead-of-exception + "TRY401", # verbose-log-message + "UP", # pyupgrade rules + "W191", # tab-indentation + "W605", # invalid-escape-sequence # security related linting rules # RCE proctection (sort of) "S102", # exec-builtin, disallow use of `exec` "S307", # suspicious-eval-usage, disallow use of `eval` and `ast.literal_eval` "S301", # suspicious-pickle-usage, disallow use of `pickle` and its wrappers. "S302", # suspicious-marshal-usage, disallow use of `marshal` module + "S311", # suspicious-non-cryptographic-random-usage ] ignore = [ - "E402", # module-import-not-at-top-of-file - "E711", # none-comparison - "E712", # true-false-comparison - "E721", # type-comparison - "E722", # bare-except - "F821", # undefined-name - "F841", # unused-variable + "E402", # module-import-not-at-top-of-file + "E711", # none-comparison + "E712", # true-false-comparison + "E721", # type-comparison + "E722", # bare-except + "F821", # undefined-name + "F841", # unused-variable "FURB113", # repeated-append "FURB152", # math-constant - "UP007", # non-pep604-annotation - "UP032", # f-string - "UP045", # non-pep604-annotation-optional - "B005", # strip-with-multi-characters - "B006", # mutable-argument-default - "B007", # unused-loop-control-variable - "B026", # star-arg-unpacking-after-keyword-arg - "B903", # class-as-data-structure - "B904", # raise-without-from-inside-except - "B905", # zip-without-explicit-strict - "N806", # non-lowercase-variable-in-function - "N815", # mixed-case-variable-in-class-scope - "PT011", # pytest-raises-too-broad - "SIM102", # collapsible-if - "SIM103", # needless-bool - "SIM105", # suppressible-exception - "SIM107", # return-in-try-except-finally - "SIM108", # if-else-block-instead-of-if-exp - "SIM113", # enumerate-for-loop - "SIM117", # multiple-with-statements - "SIM210", # if-expr-with-true-false + "UP007", # non-pep604-annotation + "UP032", # f-string + "UP045", # non-pep604-annotation-optional + "B005", # strip-with-multi-characters + "B006", # mutable-argument-default + "B007", # unused-loop-control-variable + "B026", # star-arg-unpacking-after-keyword-arg + "B903", # class-as-data-structure + "B904", # raise-without-from-inside-except + "B905", # zip-without-explicit-strict + "N806", # non-lowercase-variable-in-function + "N815", # mixed-case-variable-in-class-scope + "PT011", # pytest-raises-too-broad + "SIM102", # collapsible-if + "SIM103", # needless-bool + "SIM105", # suppressible-exception + "SIM107", # return-in-try-except-finally + "SIM108", # if-else-block-instead-of-if-exp + "SIM113", # enumerate-for-loop + "SIM117", # multiple-with-statements + "SIM210", # if-expr-with-true-false + "UP038", # deprecated and not recommended by Ruff, https://docs.astral.sh/ruff/rules/non-pep604-isinstance/ ] [lint.per-file-ignores] diff --git a/api/Dockerfile b/api/Dockerfile index cff696ff56..7e4997507f 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -4,7 +4,7 @@ FROM python:3.12-slim-bookworm AS base WORKDIR /app/api # Install uv -ENV UV_VERSION=0.6.14 +ENV UV_VERSION=0.7.11 RUN pip install --no-cache-dir uv==${UV_VERSION} diff --git a/api/commands.py b/api/commands.py index 6262bfde6b..86769847c1 100644 --- a/api/commands.py +++ b/api/commands.py @@ -27,7 +27,7 @@ from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, D from models.dataset import Document as DatasetDocument from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation from models.provider import Provider, ProviderModel -from services.account_service import RegisterService, TenantService +from services.account_service import AccountService, RegisterService, TenantService from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs from services.plugin.data_migration import PluginDataMigration from services.plugin.plugin_migration import PluginMigration @@ -68,6 +68,7 @@ def reset_password(email, new_password, password_confirm): account.password = base64_password_hashed account.password_salt = base64_salt db.session.commit() + AccountService.reset_login_error_rate_limit(email) click.echo(click.style("Password reset successfully.", fg="green")) @@ -280,6 +281,7 @@ def migrate_knowledge_vector_database(): VectorType.ELASTICSEARCH, VectorType.OPENGAUSS, VectorType.TABLESTORE, + VectorType.MATRIXONE, } lower_collection_vector_types = { VectorType.ANALYTICDB, diff --git a/api/configs/app_config.py b/api/configs/app_config.py index 3a3ad35ee7..20f8c40427 100644 --- a/api/configs/app_config.py +++ b/api/configs/app_config.py @@ -1,8 +1,11 @@ import logging +from pathlib import Path from typing import Any from pydantic.fields import FieldInfo -from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict +from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource + +from libs.file_utils import search_file_upwards from .deploy import DeploymentConfig from .enterprise import EnterpriseFeatureConfig @@ -99,4 +102,12 @@ class DifyConfig( RemoteSettingsSourceFactory(settings_cls), dotenv_settings, file_secret_settings, + TomlConfigSettingsSource( + settings_cls=settings_cls, + toml_file=search_file_upwards( + base_dir_path=Path(__file__).parent, + target_file_name="pyproject.toml", + max_search_parent_depth=2, + ), + ), ) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index a3da5c1b49..f6a8b037ca 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -237,6 +237,13 @@ class FileAccessConfig(BaseSettings): default="", ) + INTERNAL_FILES_URL: str = Field( + description="Internal base URL for file access within Docker network," + " used for plugin daemon and internal service communication." + " Falls back to FILES_URL if not specified.", + default="", + ) + FILES_ACCESS_TIMEOUT: int = Field( description="Expiration time in seconds for file access URLs", default=300, @@ -530,6 +537,33 @@ class WorkflowNodeExecutionConfig(BaseSettings): ) +class RepositoryConfig(BaseSettings): + """ + Configuration for repository implementations + """ + + CORE_WORKFLOW_EXECUTION_REPOSITORY: str = Field( + description="Repository implementation for WorkflowExecution. Specify as a module path", + default="core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository", + ) + + CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: str = Field( + description="Repository implementation for WorkflowNodeExecution. Specify as a module path", + default="core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository", + ) + + API_WORKFLOW_NODE_EXECUTION_REPOSITORY: str = Field( + description="Service-layer repository implementation for WorkflowNodeExecutionModel operations. " + "Specify as a module path", + default="repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository", + ) + + API_WORKFLOW_RUN_REPOSITORY: str = Field( + description="Service-layer repository implementation for WorkflowRun operations. Specify as a module path", + default="repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository", + ) + + class AuthConfig(BaseSettings): """ Configuration for authentication and OAuth @@ -609,7 +643,7 @@ class MailConfig(BaseSettings): """ MAIL_TYPE: Optional[str] = Field( - description="Email service provider type ('smtp' or 'resend'), default to None.", + description="Email service provider type ('smtp' or 'resend' or 'sendGrid), default to None.", default=None, ) @@ -663,6 +697,11 @@ class MailConfig(BaseSettings): default=50, ) + SENDGRID_API_KEY: Optional[str] = Field( + description="API key for SendGrid service", + default=None, + ) + class RagEtlConfig(BaseSettings): """ @@ -891,6 +930,7 @@ class FeatureConfig( MultiModalTransferConfig, PositionConfig, RagEtlConfig, + RepositoryConfig, SecurityConfig, ToolConfig, UpdateConfig, diff --git a/api/configs/middleware/__init__.py b/api/configs/middleware/__init__.py index 2dcf1710b0..0c0c06dd46 100644 --- a/api/configs/middleware/__init__.py +++ b/api/configs/middleware/__init__.py @@ -24,6 +24,7 @@ from .vdb.couchbase_config import CouchbaseConfig from .vdb.elasticsearch_config import ElasticsearchConfig from .vdb.huawei_cloud_config import HuaweiCloudConfig from .vdb.lindorm_config import LindormConfig +from .vdb.matrixone_config import MatrixoneConfig from .vdb.milvus_config import MilvusConfig from .vdb.myscale_config import MyScaleConfig from .vdb.oceanbase_config import OceanBaseVectorConfig @@ -161,6 +162,11 @@ class DatabaseConfig(BaseSettings): default=3600, ) + SQLALCHEMY_POOL_USE_LIFO: bool = Field( + description="If True, SQLAlchemy will use last-in-first-out way to retrieve connections from pool.", + default=False, + ) + SQLALCHEMY_POOL_PRE_PING: bool = Field( description="If True, enables connection pool pre-ping feature to check connections.", default=False, @@ -198,6 +204,7 @@ class DatabaseConfig(BaseSettings): "pool_recycle": self.SQLALCHEMY_POOL_RECYCLE, "pool_pre_ping": self.SQLALCHEMY_POOL_PRE_PING, "connect_args": connect_args, + "pool_use_lifo": self.SQLALCHEMY_POOL_USE_LIFO, } @@ -222,6 +229,10 @@ class CeleryConfig(DatabaseConfig): default=None, ) + CELERY_SENTINEL_PASSWORD: Optional[str] = Field( + description="Password of the Redis Sentinel master.", + default=None, + ) CELERY_SENTINEL_SOCKET_TIMEOUT: Optional[PositiveFloat] = Field( description="Timeout for Redis Sentinel socket operations in seconds.", default=0.1, @@ -323,5 +334,6 @@ class MiddlewareConfig( OpenGaussConfig, TableStoreConfig, DatasetQueueMonitorConfig, + MatrixoneConfig, ): pass diff --git a/api/configs/middleware/vdb/matrixone_config.py b/api/configs/middleware/vdb/matrixone_config.py new file mode 100644 index 0000000000..9400612d8e --- /dev/null +++ b/api/configs/middleware/vdb/matrixone_config.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field + + +class MatrixoneConfig(BaseModel): + """Matrixone vector database configuration.""" + + MATRIXONE_HOST: str = Field(default="localhost", description="Host address of the Matrixone server") + MATRIXONE_PORT: int = Field(default=6001, description="Port number of the Matrixone server") + MATRIXONE_USER: str = Field(default="dump", description="Username for authenticating with Matrixone") + MATRIXONE_PASSWORD: str = Field(default="111", description="Password for authenticating with Matrixone") + MATRIXONE_DATABASE: str = Field(default="dify", description="Name of the Matrixone database to connect to") + MATRIXONE_METRIC: str = Field( + default="l2", description="Distance metric type for vector similarity search (cosine or l2)" + ) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index ae4875d591..f511e20e6b 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -1,17 +1,13 @@ from pydantic import Field -from pydantic_settings import BaseSettings +from configs.packaging.pyproject import PyProjectConfig, PyProjectTomlConfig -class PackagingInfo(BaseSettings): + +class PackagingInfo(PyProjectTomlConfig): """ Packaging build information """ - CURRENT_VERSION: str = Field( - description="Dify version", - default="1.4.1", - ) - COMMIT_SHA: str = Field( description="SHA-1 checksum of the git commit used to build the app", default="", diff --git a/api/configs/packaging/pyproject.py b/api/configs/packaging/pyproject.py new file mode 100644 index 0000000000..90b1ecba06 --- /dev/null +++ b/api/configs/packaging/pyproject.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings + + +class PyProjectConfig(BaseModel): + version: str = Field(description="Dify version", default="") + + +class PyProjectTomlConfig(BaseSettings): + """ + configs in api/pyproject.toml + """ + + project: PyProjectConfig = Field( + description="configs in the project section of pyproject.toml", + default=PyProjectConfig(), + ) diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index a974c63e35..e25f92399c 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -56,6 +56,7 @@ from .app import ( conversation, conversation_variables, generator, + mcp_server, message, model_config, ops_trace, @@ -63,6 +64,7 @@ from .app import ( statistic, workflow, workflow_app_log, + workflow_draft_variable, workflow_run, workflow_statistic, ) diff --git a/api/controllers/console/admin.py b/api/controllers/console/admin.py index 8cb7ad9f5b..f5257fae79 100644 --- a/api/controllers/console/admin.py +++ b/api/controllers/console/admin.py @@ -56,8 +56,7 @@ class InsertExploreAppListApi(Resource): parser.add_argument("position", type=int, required=True, nullable=False, location="json") args = parser.parse_args() - with Session(db.engine) as session: - app = session.execute(select(App).filter(App.id == args["app_id"])).scalar_one_or_none() + app = db.session.execute(select(App).filter(App.id == args["app_id"])).scalar_one_or_none() if not app: raise NotFound(f"App '{args['app_id']}' is not found") @@ -78,38 +77,38 @@ class InsertExploreAppListApi(Resource): select(RecommendedApp).filter(RecommendedApp.app_id == args["app_id"]) ).scalar_one_or_none() - if not recommended_app: - recommended_app = RecommendedApp( - app_id=app.id, - description=desc, - copyright=copy_right, - privacy_policy=privacy_policy, - custom_disclaimer=custom_disclaimer, - language=args["language"], - category=args["category"], - position=args["position"], - ) - - db.session.add(recommended_app) - - app.is_public = True - db.session.commit() - - return {"result": "success"}, 201 - else: - recommended_app.description = desc - recommended_app.copyright = copy_right - recommended_app.privacy_policy = privacy_policy - recommended_app.custom_disclaimer = custom_disclaimer - recommended_app.language = args["language"] - recommended_app.category = args["category"] - recommended_app.position = args["position"] + if not recommended_app: + recommended_app = RecommendedApp( + app_id=app.id, + description=desc, + copyright=copy_right, + privacy_policy=privacy_policy, + custom_disclaimer=custom_disclaimer, + language=args["language"], + category=args["category"], + position=args["position"], + ) + + db.session.add(recommended_app) + + app.is_public = True + db.session.commit() + + return {"result": "success"}, 201 + else: + recommended_app.description = desc + recommended_app.copyright = copy_right + recommended_app.privacy_policy = privacy_policy + recommended_app.custom_disclaimer = custom_disclaimer + recommended_app.language = args["language"] + recommended_app.category = args["category"] + recommended_app.position = args["position"] - app.is_public = True + app.is_public = True - db.session.commit() + db.session.commit() - return {"result": "success"}, 200 + return {"result": "success"}, 200 class InsertExploreAppApi(Resource): diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py index 91058767eb..2b48afd550 100644 --- a/api/controllers/console/app/annotation.py +++ b/api/controllers/console/app/annotation.py @@ -208,7 +208,7 @@ class AnnotationBatchImportApi(Resource): if len(request.files) > 1: raise TooManyFilesError() # check file type - if not file.filename.endswith(".csv"): + if not file.filename or not file.filename.lower().endswith(".csv"): raise ValueError("Invalid file type. Only CSV files are allowed") return AppAnnotationService.batch_import_app_annotations(app_id, file) diff --git a/api/controllers/console/app/app_import.py b/api/controllers/console/app/app_import.py index 5dc6515ce0..9ffb94e9f9 100644 --- a/api/controllers/console/app/app_import.py +++ b/api/controllers/console/app/app_import.py @@ -17,6 +17,8 @@ from libs.login import login_required from models import Account from models.model import App from services.app_dsl_service import AppDslService, ImportStatus +from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService class AppImportApi(Resource): @@ -60,7 +62,9 @@ class AppImportApi(Resource): app_id=args.get("app_id"), ) session.commit() - + if result.app_id and FeatureService.get_system_features().webapp_auth.enabled: + # update web app setting as private + EnterpriseService.WebAppAuth.update_app_access_mode(result.app_id, "private") # Return appropriate status code based on result status = result.status if status == ImportStatus.FAILED.value: diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index 5f2def8d8e..665cf1aede 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -90,23 +90,11 @@ class ChatMessageTextApi(Resource): message_id = args.get("message_id", None) text = args.get("text", None) - if ( - app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value} - and app_model.workflow - and app_model.workflow.features_dict - ): - text_to_speech = app_model.workflow.features_dict.get("text_to_speech") - if text_to_speech is None: - raise ValueError("TTS is not enabled") - voice = args.get("voice") or text_to_speech.get("voice") - else: - try: - if app_model.app_model_config is None: - raise ValueError("AppModelConfig not found") - voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") - except Exception: - voice = None - response = AudioService.transcript_tts(app_model=app_model, text=text, message_id=message_id, voice=voice) + voice = args.get("voice", None) + + response = AudioService.transcript_tts( + app_model=app_model, text=text, voice=voice, message_id=message_id, is_draft=True + ) return response except services.errors.app_model_config.AppModelConfigBrokenError: logging.exception("App model config broken.") diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py new file mode 100644 index 0000000000..0f53860f56 --- /dev/null +++ b/api/controllers/console/app/mcp_server.py @@ -0,0 +1,107 @@ +import json +from enum import StrEnum + +from flask_login import current_user +from flask_restful import Resource, marshal_with, reqparse +from werkzeug.exceptions import NotFound + +from controllers.console import api +from controllers.console.app.wraps import get_app_model +from controllers.console.wraps import account_initialization_required, setup_required +from extensions.ext_database import db +from fields.app_fields import app_server_fields +from libs.login import login_required +from models.model import AppMCPServer + + +class AppMCPServerStatus(StrEnum): + ACTIVE = "active" + INACTIVE = "inactive" + + +class AppMCPServerController(Resource): + @setup_required + @login_required + @account_initialization_required + @get_app_model + @marshal_with(app_server_fields) + def get(self, app_model): + server = db.session.query(AppMCPServer).filter(AppMCPServer.app_id == app_model.id).first() + return server + + @setup_required + @login_required + @account_initialization_required + @get_app_model + @marshal_with(app_server_fields) + def post(self, app_model): + # The role of the current user in the ta table must be editor, admin, or owner + if not current_user.is_editor: + raise NotFound() + parser = reqparse.RequestParser() + parser.add_argument("description", type=str, required=True, location="json") + parser.add_argument("parameters", type=dict, required=True, location="json") + args = parser.parse_args() + server = AppMCPServer( + name=app_model.name, + description=args["description"], + parameters=json.dumps(args["parameters"], ensure_ascii=False), + status=AppMCPServerStatus.ACTIVE, + app_id=app_model.id, + tenant_id=current_user.current_tenant_id, + server_code=AppMCPServer.generate_server_code(16), + ) + db.session.add(server) + db.session.commit() + return server + + @setup_required + @login_required + @account_initialization_required + @get_app_model + @marshal_with(app_server_fields) + def put(self, app_model): + if not current_user.is_editor: + raise NotFound() + parser = reqparse.RequestParser() + parser.add_argument("id", type=str, required=True, location="json") + parser.add_argument("description", type=str, required=True, location="json") + parser.add_argument("parameters", type=dict, required=True, location="json") + parser.add_argument("status", type=str, required=False, location="json") + args = parser.parse_args() + server = db.session.query(AppMCPServer).filter(AppMCPServer.id == args["id"]).first() + if not server: + raise NotFound() + server.description = args["description"] + server.parameters = json.dumps(args["parameters"], ensure_ascii=False) + if args["status"]: + if args["status"] not in [status.value for status in AppMCPServerStatus]: + raise ValueError("Invalid status") + server.status = args["status"] + db.session.commit() + return server + + +class AppMCPServerRefreshController(Resource): + @setup_required + @login_required + @account_initialization_required + @marshal_with(app_server_fields) + def get(self, server_id): + if not current_user.is_editor: + raise NotFound() + server = ( + db.session.query(AppMCPServer) + .filter(AppMCPServer.id == server_id) + .filter(AppMCPServer.tenant_id == current_user.current_tenant_id) + .first() + ) + if not server: + raise NotFound() + server.server_code = AppMCPServer.generate_server_code(16) + db.session.commit() + return server + + +api.add_resource(AppMCPServerController, "/apps//server") +api.add_resource(AppMCPServerRefreshController, "/apps//server/refresh") diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index 86aed77412..32b64d10c5 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -2,6 +2,7 @@ from datetime import datetime from decimal import Decimal import pytz +import sqlalchemy as sa from flask import jsonify from flask_login import current_user from flask_restful import Resource, reqparse @@ -9,10 +10,11 @@ from flask_restful import Resource, reqparse from controllers.console import api from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required +from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from libs.helper import DatetimeString from libs.login import login_required -from models.model import AppMode +from models import AppMode, Message class DailyMessageStatistic(Resource): @@ -85,46 +87,41 @@ class DailyConversationStatistic(Resource): parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args") args = parser.parse_args() - sql_query = """SELECT - DATE(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, - COUNT(DISTINCT messages.conversation_id) AS conversation_count -FROM - messages -WHERE - app_id = :app_id""" - arg_dict = {"tz": account.timezone, "app_id": app_model.id} - timezone = pytz.timezone(account.timezone) utc_timezone = pytz.utc + stmt = ( + sa.select( + sa.func.date( + sa.func.date_trunc("day", sa.text("created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz")) + ).label("date"), + sa.func.count(sa.distinct(Message.conversation_id)).label("conversation_count"), + ) + .select_from(Message) + .where(Message.app_id == app_model.id, Message.invoke_from != InvokeFrom.DEBUGGER.value) + ) + if args["start"]: start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M") start_datetime = start_datetime.replace(second=0) - start_datetime_timezone = timezone.localize(start_datetime) start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone) - - sql_query += " AND created_at >= :start" - arg_dict["start"] = start_datetime_utc + stmt = stmt.where(Message.created_at >= start_datetime_utc) if args["end"]: end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M") end_datetime = end_datetime.replace(second=0) - end_datetime_timezone = timezone.localize(end_datetime) end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone) + stmt = stmt.where(Message.created_at < end_datetime_utc) - sql_query += " AND created_at < :end" - arg_dict["end"] = end_datetime_utc - - sql_query += " GROUP BY date ORDER BY date" + stmt = stmt.group_by("date").order_by("date") response_data = [] - with db.engine.begin() as conn: - rs = conn.execute(db.text(sql_query), arg_dict) - for i in rs: - response_data.append({"date": str(i.date), "conversation_count": i.conversation_count}) + rs = conn.execute(stmt, {"tz": account.timezone}) + for row in rs: + response_data.append({"date": str(row.date), "conversation_count": row.conversation_count}) return jsonify({"data": response_data}) diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index cbbdd324ba..a9f088a276 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -1,5 +1,6 @@ import json import logging +from collections.abc import Sequence from typing import cast from flask import abort, request @@ -18,10 +19,12 @@ from controllers.console.app.error import ( from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom +from core.file.models import File from extensions.ext_database import db -from factories import variable_factory +from factories import file_factory, variable_factory from fields.workflow_fields import workflow_fields, workflow_pagination_fields from fields.workflow_run_fields import workflow_run_node_execution_fields from libs import helper @@ -30,6 +33,7 @@ from libs.login import current_user, login_required from models import App from models.account import Account from models.model import AppMode +from models.workflow import Workflow from services.app_generate_service import AppGenerateService from services.errors.app import WorkflowHashNotEqualError from services.errors.llm import InvokeRateLimitError @@ -38,6 +42,24 @@ from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseE logger = logging.getLogger(__name__) +# TODO(QuantumGhost): Refactor existing node run API to handle file parameter parsing +# at the controller level rather than in the workflow logic. This would improve separation +# of concerns and make the code more maintainable. +def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence[File]: + files = files or [] + + file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) + file_objs: Sequence[File] = [] + if file_extra_config is None: + return file_objs + file_objs = file_factory.build_from_mappings( + mappings=files, + tenant_id=workflow.tenant_id, + config=file_extra_config, + ) + return file_objs + + class DraftWorkflowApi(Resource): @setup_required @login_required @@ -402,15 +424,30 @@ class DraftWorkflowNodeRunApi(Resource): parser = reqparse.RequestParser() parser.add_argument("inputs", type=dict, required=True, nullable=False, location="json") + parser.add_argument("query", type=str, required=False, location="json", default="") + parser.add_argument("files", type=list, location="json", default=[]) args = parser.parse_args() - inputs = args.get("inputs") - if inputs == None: + user_inputs = args.get("inputs") + if user_inputs is None: raise ValueError("missing inputs") + workflow_srv = WorkflowService() + # fetch draft workflow by app_model + draft_workflow = workflow_srv.get_draft_workflow(app_model=app_model) + if not draft_workflow: + raise ValueError("Workflow not initialized") + files = _parse_file(draft_workflow, args.get("files")) workflow_service = WorkflowService() + workflow_node_execution = workflow_service.run_draft_workflow_node( - app_model=app_model, node_id=node_id, user_inputs=inputs, account=current_user + app_model=app_model, + draft_workflow=draft_workflow, + node_id=node_id, + user_inputs=user_inputs, + account=current_user, + query=args.get("query", ""), + files=files, ) return workflow_node_execution @@ -731,6 +768,27 @@ class WorkflowByIdApi(Resource): return None, 204 +class DraftWorkflowNodeLastRunApi(Resource): + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @marshal_with(workflow_run_node_execution_fields) + def get(self, app_model: App, node_id: str): + srv = WorkflowService() + workflow = srv.get_draft_workflow(app_model) + if not workflow: + raise NotFound("Workflow not found") + node_exec = srv.get_node_last_run( + app_model=app_model, + workflow=workflow, + node_id=node_id, + ) + if node_exec is None: + raise NotFound("last run not found") + return node_exec + + api.add_resource( DraftWorkflowApi, "/apps//workflows/draft", @@ -795,3 +853,7 @@ api.add_resource( WorkflowByIdApi, "/apps//workflows/", ) +api.add_resource( + DraftWorkflowNodeLastRunApi, + "/apps//workflows/draft/nodes//last-run", +) diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index b9579e2120..310146a5e7 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -34,6 +34,20 @@ class WorkflowAppLogApi(Resource): parser.add_argument( "created_at__after", type=str, location="args", help="Filter logs created after this timestamp" ) + parser.add_argument( + "created_by_end_user_session_id", + type=str, + location="args", + required=False, + default=None, + ) + parser.add_argument( + "created_by_account", + type=str, + location="args", + required=False, + default=None, + ) parser.add_argument("page", type=int_range(1, 99999), default=1, location="args") parser.add_argument("limit", type=int_range(1, 100), default=20, location="args") args = parser.parse_args() @@ -57,6 +71,8 @@ class WorkflowAppLogApi(Resource): created_at_after=args.created_at__after, page=args.page, limit=args.limit, + created_by_end_user_session_id=args.created_by_end_user_session_id, + created_by_account=args.created_by_account, ) return workflow_app_log_pagination diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py new file mode 100644 index 0000000000..ba93f82756 --- /dev/null +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -0,0 +1,426 @@ +import logging +from typing import Any, NoReturn + +from flask import Response +from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse +from sqlalchemy.orm import Session +from werkzeug.exceptions import Forbidden + +from controllers.console import api +from controllers.console.app.error import ( + DraftWorkflowNotExist, +) +from controllers.console.app.wraps import get_app_model +from controllers.console.wraps import account_initialization_required, setup_required +from controllers.web.error import InvalidArgumentError, NotFoundError +from core.variables.segment_group import SegmentGroup +from core.variables.segments import ArrayFileSegment, FileSegment, Segment +from core.variables.types import SegmentType +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from factories.file_factory import build_from_mapping, build_from_mappings +from factories.variable_factory import build_segment_with_type +from libs.login import current_user, login_required +from models import App, AppMode, db +from models.workflow import WorkflowDraftVariable +from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService +from services.workflow_service import WorkflowService + +logger = logging.getLogger(__name__) + + +def _convert_values_to_json_serializable_object(value: Segment) -> Any: + if isinstance(value, FileSegment): + return value.value.model_dump() + elif isinstance(value, ArrayFileSegment): + return [i.model_dump() for i in value.value] + elif isinstance(value, SegmentGroup): + return [_convert_values_to_json_serializable_object(i) for i in value.value] + else: + return value.value + + +def _serialize_var_value(variable: WorkflowDraftVariable) -> Any: + value = variable.get_value() + # create a copy of the value to avoid affecting the model cache. + value = value.model_copy(deep=True) + # Refresh the url signature before returning it to client. + if isinstance(value, FileSegment): + file = value.value + file.remote_url = file.generate_url() + elif isinstance(value, ArrayFileSegment): + files = value.value + for file in files: + file.remote_url = file.generate_url() + return _convert_values_to_json_serializable_object(value) + + +def _create_pagination_parser(): + parser = reqparse.RequestParser() + parser.add_argument( + "page", + type=inputs.int_range(1, 100_000), + required=False, + default=1, + location="args", + help="the page of data requested", + ) + parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args") + return parser + + +def _serialize_variable_type(workflow_draft_var: WorkflowDraftVariable) -> str: + value_type = workflow_draft_var.value_type + return value_type.exposed_type().value + + +_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS = { + "id": fields.String, + "type": fields.String(attribute=lambda model: model.get_variable_type()), + "name": fields.String, + "description": fields.String, + "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), + "value_type": fields.String(attribute=_serialize_variable_type), + "edited": fields.Boolean(attribute=lambda model: model.edited), + "visible": fields.Boolean, +} + +_WORKFLOW_DRAFT_VARIABLE_FIELDS = dict( + _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS, + value=fields.Raw(attribute=_serialize_var_value), +) + +_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS = { + "id": fields.String, + "type": fields.String(attribute=lambda _: "env"), + "name": fields.String, + "description": fields.String, + "selector": fields.List(fields.String, attribute=lambda model: model.get_selector()), + "value_type": fields.String(attribute=_serialize_variable_type), + "edited": fields.Boolean(attribute=lambda model: model.edited), + "visible": fields.Boolean, +} + +_WORKFLOW_DRAFT_ENV_VARIABLE_LIST_FIELDS = { + "items": fields.List(fields.Nested(_WORKFLOW_DRAFT_ENV_VARIABLE_FIELDS)), +} + + +def _get_items(var_list: WorkflowDraftVariableList) -> list[WorkflowDraftVariable]: + return var_list.variables + + +_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS = { + "items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS), attribute=_get_items), + "total": fields.Raw(), +} + +_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS = { + "items": fields.List(fields.Nested(_WORKFLOW_DRAFT_VARIABLE_FIELDS), attribute=_get_items), +} + + +def _api_prerequisite(f): + """Common prerequisites for all draft workflow variable APIs. + + It ensures the following conditions are satisfied: + + - Dify has been property setup. + - The request user has logged in and initialized. + - The requested app is a workflow or a chat flow. + - The request user has the edit permission for the app. + """ + + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + def wrapper(*args, **kwargs): + if not current_user.is_editor: + raise Forbidden() + return f(*args, **kwargs) + + return wrapper + + +class WorkflowVariableCollectionApi(Resource): + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS) + def get(self, app_model: App): + """ + Get draft workflow + """ + parser = _create_pagination_parser() + args = parser.parse_args() + + # fetch draft workflow by app_model + workflow_service = WorkflowService() + workflow_exist = workflow_service.is_workflow_exist(app_model=app_model) + if not workflow_exist: + raise DraftWorkflowNotExist() + + # fetch draft workflow by app_model + with Session(bind=db.engine, expire_on_commit=False) as session: + draft_var_srv = WorkflowDraftVariableService( + session=session, + ) + workflow_vars = draft_var_srv.list_variables_without_values( + app_id=app_model.id, + page=args.page, + limit=args.limit, + ) + + return workflow_vars + + @_api_prerequisite + def delete(self, app_model: App): + draft_var_srv = WorkflowDraftVariableService( + session=db.session(), + ) + draft_var_srv.delete_workflow_variables(app_model.id) + db.session.commit() + return Response("", 204) + + +def validate_node_id(node_id: str) -> NoReturn | None: + if node_id in [ + CONVERSATION_VARIABLE_NODE_ID, + SYSTEM_VARIABLE_NODE_ID, + ]: + # NOTE(QuantumGhost): While we store the system and conversation variables as node variables + # with specific `node_id` in database, we still want to make the API separated. By disallowing + # accessing system and conversation variables in `WorkflowDraftNodeVariableListApi`, + # we mitigate the risk that user of the API depending on the implementation detail of the API. + # + # ref: [Hyrum's Law](https://www.hyrumslaw.com/) + + raise InvalidArgumentError( + f"invalid node_id, please use correspond api for conversation and system variables, node_id={node_id}", + ) + return None + + +class NodeVariableCollectionApi(Resource): + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + def get(self, app_model: App, node_id: str): + validate_node_id(node_id) + with Session(bind=db.engine, expire_on_commit=False) as session: + draft_var_srv = WorkflowDraftVariableService( + session=session, + ) + node_vars = draft_var_srv.list_node_variables(app_model.id, node_id) + + return node_vars + + @_api_prerequisite + def delete(self, app_model: App, node_id: str): + validate_node_id(node_id) + srv = WorkflowDraftVariableService(db.session()) + srv.delete_node_variables(app_model.id, node_id) + db.session.commit() + return Response("", 204) + + +class VariableApi(Resource): + _PATCH_NAME_FIELD = "name" + _PATCH_VALUE_FIELD = "value" + + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS) + def get(self, app_model: App, variable_id: str): + draft_var_srv = WorkflowDraftVariableService( + session=db.session(), + ) + variable = draft_var_srv.get_variable(variable_id=variable_id) + if variable is None: + raise NotFoundError(description=f"variable not found, id={variable_id}") + if variable.app_id != app_model.id: + raise NotFoundError(description=f"variable not found, id={variable_id}") + return variable + + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS) + def patch(self, app_model: App, variable_id: str): + # Request payload for file types: + # + # Local File: + # + # { + # "type": "image", + # "transfer_method": "local_file", + # "url": "", + # "upload_file_id": "daded54f-72c7-4f8e-9d18-9b0abdd9f190" + # } + # + # Remote File: + # + # + # { + # "type": "image", + # "transfer_method": "remote_url", + # "url": "http://127.0.0.1:5001/files/1602650a-4fe4-423c-85a2-af76c083e3c4/file-preview?timestamp=1750041099&nonce=...&sign=...=", + # "upload_file_id": "1602650a-4fe4-423c-85a2-af76c083e3c4" + # } + + parser = reqparse.RequestParser() + parser.add_argument(self._PATCH_NAME_FIELD, type=str, required=False, nullable=True, location="json") + # Parse 'value' field as-is to maintain its original data structure + parser.add_argument(self._PATCH_VALUE_FIELD, type=lambda x: x, required=False, nullable=True, location="json") + + draft_var_srv = WorkflowDraftVariableService( + session=db.session(), + ) + args = parser.parse_args(strict=True) + + variable = draft_var_srv.get_variable(variable_id=variable_id) + if variable is None: + raise NotFoundError(description=f"variable not found, id={variable_id}") + if variable.app_id != app_model.id: + raise NotFoundError(description=f"variable not found, id={variable_id}") + + new_name = args.get(self._PATCH_NAME_FIELD, None) + raw_value = args.get(self._PATCH_VALUE_FIELD, None) + if new_name is None and raw_value is None: + return variable + + new_value = None + if raw_value is not None: + if variable.value_type == SegmentType.FILE: + if not isinstance(raw_value, dict): + raise InvalidArgumentError(description=f"expected dict for file, got {type(raw_value)}") + raw_value = build_from_mapping(mapping=raw_value, tenant_id=app_model.tenant_id) + elif variable.value_type == SegmentType.ARRAY_FILE: + if not isinstance(raw_value, list): + raise InvalidArgumentError(description=f"expected list for files, got {type(raw_value)}") + if len(raw_value) > 0 and not isinstance(raw_value[0], dict): + raise InvalidArgumentError(description=f"expected dict for files[0], got {type(raw_value)}") + raw_value = build_from_mappings(mappings=raw_value, tenant_id=app_model.tenant_id) + new_value = build_segment_with_type(variable.value_type, raw_value) + draft_var_srv.update_variable(variable, name=new_name, value=new_value) + db.session.commit() + return variable + + @_api_prerequisite + def delete(self, app_model: App, variable_id: str): + draft_var_srv = WorkflowDraftVariableService( + session=db.session(), + ) + variable = draft_var_srv.get_variable(variable_id=variable_id) + if variable is None: + raise NotFoundError(description=f"variable not found, id={variable_id}") + if variable.app_id != app_model.id: + raise NotFoundError(description=f"variable not found, id={variable_id}") + draft_var_srv.delete_variable(variable) + db.session.commit() + return Response("", 204) + + +class VariableResetApi(Resource): + @_api_prerequisite + def put(self, app_model: App, variable_id: str): + draft_var_srv = WorkflowDraftVariableService( + session=db.session(), + ) + + workflow_srv = WorkflowService() + draft_workflow = workflow_srv.get_draft_workflow(app_model) + if draft_workflow is None: + raise NotFoundError( + f"Draft workflow not found, app_id={app_model.id}", + ) + variable = draft_var_srv.get_variable(variable_id=variable_id) + if variable is None: + raise NotFoundError(description=f"variable not found, id={variable_id}") + if variable.app_id != app_model.id: + raise NotFoundError(description=f"variable not found, id={variable_id}") + + resetted = draft_var_srv.reset_variable(draft_workflow, variable) + db.session.commit() + if resetted is None: + return Response("", 204) + else: + return marshal(resetted, _WORKFLOW_DRAFT_VARIABLE_FIELDS) + + +def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList: + with Session(bind=db.engine, expire_on_commit=False) as session: + draft_var_srv = WorkflowDraftVariableService( + session=session, + ) + if node_id == CONVERSATION_VARIABLE_NODE_ID: + draft_vars = draft_var_srv.list_conversation_variables(app_model.id) + elif node_id == SYSTEM_VARIABLE_NODE_ID: + draft_vars = draft_var_srv.list_system_variables(app_model.id) + else: + draft_vars = draft_var_srv.list_node_variables(app_id=app_model.id, node_id=node_id) + return draft_vars + + +class ConversationVariableCollectionApi(Resource): + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + def get(self, app_model: App): + # NOTE(QuantumGhost): Prefill conversation variables into the draft variables table + # so their IDs can be returned to the caller. + workflow_srv = WorkflowService() + draft_workflow = workflow_srv.get_draft_workflow(app_model) + if draft_workflow is None: + raise NotFoundError(description=f"draft workflow not found, id={app_model.id}") + draft_var_srv = WorkflowDraftVariableService(db.session()) + draft_var_srv.prefill_conversation_variable_default_values(draft_workflow) + db.session.commit() + return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID) + + +class SystemVariableCollectionApi(Resource): + @_api_prerequisite + @marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + def get(self, app_model: App): + return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID) + + +class EnvironmentVariableCollectionApi(Resource): + @_api_prerequisite + def get(self, app_model: App): + """ + Get draft workflow + """ + # fetch draft workflow by app_model + workflow_service = WorkflowService() + workflow = workflow_service.get_draft_workflow(app_model=app_model) + if workflow is None: + raise DraftWorkflowNotExist() + + env_vars = workflow.environment_variables + env_vars_list = [] + for v in env_vars: + env_vars_list.append( + { + "id": v.id, + "type": "env", + "name": v.name, + "description": v.description, + "selector": v.selector, + "value_type": v.value_type.exposed_type().value, + "value": v.value, + # Do not track edited for env vars. + "edited": False, + "visible": True, + "editable": True, + } + ) + + return {"items": env_vars_list} + + +api.add_resource( + WorkflowVariableCollectionApi, + "/apps//workflows/draft/variables", +) +api.add_resource(NodeVariableCollectionApi, "/apps//workflows/draft/nodes//variables") +api.add_resource(VariableApi, "/apps//workflows/draft/variables/") +api.add_resource(VariableResetApi, "/apps//workflows/draft/variables//reset") + +api.add_resource(ConversationVariableCollectionApi, "/apps//workflows/draft/conversation-variables") +api.add_resource(SystemVariableCollectionApi, "/apps//workflows/draft/system-variables") +api.add_resource(EnvironmentVariableCollectionApi, "/apps//workflows/draft/environment-variables") diff --git a/api/controllers/console/app/wraps.py b/api/controllers/console/app/wraps.py index 9ad8c15847..3322350e25 100644 --- a/api/controllers/console/app/wraps.py +++ b/api/controllers/console/app/wraps.py @@ -8,6 +8,15 @@ from libs.login import current_user from models import App, AppMode +def _load_app_model(app_id: str) -> Optional[App]: + app_model = ( + db.session.query(App) + .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal") + .first() + ) + return app_model + + def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode], None] = None): def decorator(view_func): @wraps(view_func) @@ -20,18 +29,12 @@ def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[ del kwargs["app_id"] - app_model = ( - db.session.query(App) - .filter(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal") - .first() - ) + app_model = _load_app_model(app_id) if not app_model: raise AppNotFoundError() app_mode = AppMode.value_of(app_model.mode) - if app_mode == AppMode.CHANNEL: - raise AppNotFoundError() if mode is not None: if isinstance(mode, list): diff --git a/api/controllers/console/auth/data_source_oauth.py b/api/controllers/console/auth/data_source_oauth.py index 1049f864c3..4c9697cc32 100644 --- a/api/controllers/console/auth/data_source_oauth.py +++ b/api/controllers/console/auth/data_source_oauth.py @@ -41,7 +41,7 @@ class OAuthDataSource(Resource): if not internal_secret: return ({"error": "Internal secret is not set"},) oauth_provider.save_internal_access_token(internal_secret) - return {"data": ""} + return {"data": "internal"} else: auth_url = oauth_provider.get_authorization_url() return {"data": auth_url}, 200 diff --git a/api/controllers/console/auth/forgot_password.py b/api/controllers/console/auth/forgot_password.py index 8db19095d2..3bbe3177fc 100644 --- a/api/controllers/console/auth/forgot_password.py +++ b/api/controllers/console/auth/forgot_password.py @@ -119,9 +119,6 @@ class ForgotPasswordResetApi(Resource): if not reset_data: raise InvalidTokenError() # Must use token in reset phase - if reset_data.get("phase", "") != "reset": - raise InvalidTokenError() - # Must use token in reset phase if reset_data.get("phase", "") != "reset": raise InvalidTokenError() diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index e68273afa6..1611214cb3 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -686,6 +686,7 @@ class DatasetRetrievalSettingApi(Resource): | VectorType.TABLESTORE | VectorType.HUAWEI_CLOUD | VectorType.TENCENT + | VectorType.MATRIXONE ): return { "retrieval_method": [ @@ -733,6 +734,7 @@ class DatasetRetrievalSettingMockApi(Resource): | VectorType.TABLESTORE | VectorType.TENCENT | VectorType.HUAWEI_CLOUD + | VectorType.MATRIXONE ): return { "retrieval_method": [ diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index f7c04102a9..b2fcf3ce7b 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -5,7 +5,7 @@ from typing import cast from flask import request from flask_login import current_user -from flask_restful import Resource, fields, marshal, marshal_with, reqparse +from flask_restful import Resource, marshal, marshal_with, reqparse from sqlalchemy import asc, desc, select from werkzeug.exceptions import Forbidden, NotFound @@ -43,7 +43,6 @@ from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.plugin.impl.exc import PluginDaemonClientSideError from core.rag.extractor.entity.extract_setting import ExtractSetting from extensions.ext_database import db -from extensions.ext_redis import redis_client from fields.document_fields import ( dataset_and_document_fields, document_fields, @@ -54,8 +53,6 @@ from libs.login import login_required from models import Dataset, DatasetProcessRule, Document, DocumentSegment, UploadFile from services.dataset_service import DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig -from tasks.add_document_to_index_task import add_document_to_index_task -from tasks.remove_document_from_index_task import remove_document_from_index_task class DocumentResource(Resource): @@ -242,12 +239,10 @@ class DatasetDocumentListApi(Resource): return response - documents_and_batch_fields = {"documents": fields.List(fields.Nested(document_fields)), "batch": fields.String} - @setup_required @login_required @account_initialization_required - @marshal_with(documents_and_batch_fields) + @marshal_with(dataset_and_document_fields) @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") def post(self, dataset_id): @@ -293,6 +288,8 @@ class DatasetDocumentListApi(Resource): try: documents, batch = DocumentService.save_document_with_dataset_id(dataset, knowledge_config, current_user) + dataset = DatasetService.get_dataset(dataset_id) + except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: @@ -300,7 +297,7 @@ class DatasetDocumentListApi(Resource): except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() - return {"documents": documents, "batch": batch} + return {"dataset": dataset, "documents": documents, "batch": batch} @setup_required @login_required @@ -862,77 +859,16 @@ class DocumentStatusApi(DocumentResource): DatasetService.check_dataset_permission(dataset, current_user) document_ids = request.args.getlist("document_id") - for document_id in document_ids: - document = self.get_document(dataset_id, document_id) - - indexing_cache_key = "document_{}_indexing".format(document.id) - cache_result = redis_client.get(indexing_cache_key) - if cache_result is not None: - raise InvalidActionError(f"Document:{document.name} is being indexed, please try again later") - - if action == "enable": - if document.enabled: - continue - document.enabled = True - document.disabled_at = None - document.disabled_by = None - document.updated_at = datetime.now(UTC).replace(tzinfo=None) - db.session.commit() - - # Set cache to prevent indexing the same document multiple times - redis_client.setex(indexing_cache_key, 600, 1) - - add_document_to_index_task.delay(document_id) - - elif action == "disable": - if not document.completed_at or document.indexing_status != "completed": - raise InvalidActionError(f"Document: {document.name} is not completed.") - if not document.enabled: - continue - - document.enabled = False - document.disabled_at = datetime.now(UTC).replace(tzinfo=None) - document.disabled_by = current_user.id - document.updated_at = datetime.now(UTC).replace(tzinfo=None) - db.session.commit() - - # Set cache to prevent indexing the same document multiple times - redis_client.setex(indexing_cache_key, 600, 1) - - remove_document_from_index_task.delay(document_id) - - elif action == "archive": - if document.archived: - continue - - document.archived = True - document.archived_at = datetime.now(UTC).replace(tzinfo=None) - document.archived_by = current_user.id - document.updated_at = datetime.now(UTC).replace(tzinfo=None) - db.session.commit() - - if document.enabled: - # Set cache to prevent indexing the same document multiple times - redis_client.setex(indexing_cache_key, 600, 1) - - remove_document_from_index_task.delay(document_id) - - elif action == "un_archive": - if not document.archived: - continue - document.archived = False - document.archived_at = None - document.archived_by = None - document.updated_at = datetime.now(UTC).replace(tzinfo=None) - db.session.commit() - - # Set cache to prevent indexing the same document multiple times - redis_client.setex(indexing_cache_key, 600, 1) - - add_document_to_index_task.delay(document_id) - else: - raise InvalidActionError() + try: + DocumentService.batch_update_document_status(dataset, document_ids, action, current_user) + except services.errors.document.DocumentIndexingError as e: + raise InvalidActionError(str(e)) + except ValueError as e: + raise InvalidActionError(str(e)) + except NotFound as e: + raise NotFound(str(e)) + return {"result": "success"}, 200 diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index eee09ac32e..48142dbe73 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -374,7 +374,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource): if len(request.files) > 1: raise TooManyFilesError() # check file type - if not file.filename.endswith(".csv"): + if not file.filename or not file.filename.lower().endswith(".csv"): raise ValueError("Invalid file type. Only CSV files are allowed") try: diff --git a/api/controllers/console/explore/audio.py b/api/controllers/console/explore/audio.py index 54bc590677..d564a00a76 100644 --- a/api/controllers/console/explore/audio.py +++ b/api/controllers/console/explore/audio.py @@ -18,7 +18,6 @@ from controllers.console.app.error import ( from controllers.console.explore.wraps import InstalledAppResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError -from models.model import AppMode from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, @@ -79,19 +78,9 @@ class ChatTextApi(InstalledAppResource): message_id = args.get("message_id", None) text = args.get("text", None) - if ( - app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value} - and app_model.workflow - and app_model.workflow.features_dict - ): - text_to_speech = app_model.workflow.features_dict.get("text_to_speech") - voice = args.get("voice") or text_to_speech.get("voice") - else: - try: - voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") - except Exception: - voice = None - response = AudioService.transcript_tts(app_model=app_model, message_id=message_id, voice=voice, text=text) + voice = args.get("voice", None) + + response = AudioService.transcript_tts(app_model=app_model, text=text, voice=voice, message_id=message_id) return response except services.errors.app_model_config.AppModelConfigBrokenError: logging.exception("App model config broken.") diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index f73a226c89..9d0c08564e 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -59,7 +59,14 @@ class InstalledAppsListApi(Resource): if FeatureService.get_system_features().webapp_auth.enabled: user_id = current_user.id res = [] + app_ids = [installed_app["app"].id for installed_app in installed_app_list] + webapp_settings = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids) for installed_app in installed_app_list: + webapp_setting = webapp_settings.get(installed_app["app"].id) + if not webapp_setting: + continue + if webapp_setting.access_mode == "sso_verified": + continue app_code = AppService.get_app_code_by_id(str(installed_app["app"].id)) if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp( user_id=user_id, diff --git a/api/controllers/console/version.py b/api/controllers/console/version.py index 7dea8e554e..447cc358f8 100644 --- a/api/controllers/console/version.py +++ b/api/controllers/console/version.py @@ -18,7 +18,7 @@ class VersionApi(Resource): check_update_url = dify_config.CHECK_UPDATE_URL result = { - "version": dify_config.CURRENT_VERSION, + "version": dify_config.project.version, "release_date": "", "release_notes": "", "can_auto_update": False, diff --git a/api/controllers/console/workspace/load_balancing_config.py b/api/controllers/console/workspace/load_balancing_config.py index ba74e2c074..b4eb5e246b 100644 --- a/api/controllers/console/workspace/load_balancing_config.py +++ b/api/controllers/console/workspace/load_balancing_config.py @@ -15,7 +15,7 @@ class LoadBalancingCredentialsValidateApi(Resource): @login_required @account_initialization_required def post(self, provider: str): - if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): + if not TenantAccountRole.is_privileged_role(current_user.current_role): raise Forbidden() tenant_id = current_user.current_tenant_id @@ -64,7 +64,7 @@ class LoadBalancingConfigCredentialsValidateApi(Resource): @login_required @account_initialization_required def post(self, provider: str, config_id: str): - if not TenantAccountRole.is_privileged_role(current_user.current_tenant.current_role): + if not TenantAccountRole.is_privileged_role(current_user.current_role): raise Forbidden() tenant_id = current_user.current_tenant_id diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index db49da7840..48225ac90d 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -85,6 +85,7 @@ class MemberInviteEmailApi(Resource): return { "result": "success", "invitation_results": invitation_results, + "tenant_id": str(current_user.current_tenant.id), }, 201 @@ -110,7 +111,7 @@ class MemberCancelInviteApi(Resource): except Exception as e: raise ValueError(str(e)) - return {"result": "success"}, 204 + return {"result": "success", "tenant_id": str(current_user.current_tenant.id)}, 200 class MemberUpdateRoleApi(Resource): diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 9bddbb4b4b..c0a4734828 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -13,6 +13,7 @@ from core.model_runtime.utils.encoders import jsonable_encoder from core.plugin.impl.exc import PluginDaemonClientSideError from libs.login import login_required from models.account import TenantPluginPermission +from services.plugin.plugin_parameter_service import PluginParameterService from services.plugin.plugin_permission_service import PluginPermissionService from services.plugin.plugin_service import PluginService @@ -497,6 +498,42 @@ class PluginFetchPermissionApi(Resource): ) +class PluginFetchDynamicSelectOptionsApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self): + # check if the user is admin or owner + if not current_user.is_admin_or_owner: + raise Forbidden() + + tenant_id = current_user.current_tenant_id + user_id = current_user.id + + parser = reqparse.RequestParser() + parser.add_argument("plugin_id", type=str, required=True, location="args") + parser.add_argument("provider", type=str, required=True, location="args") + parser.add_argument("action", type=str, required=True, location="args") + parser.add_argument("parameter", type=str, required=True, location="args") + parser.add_argument("provider_type", type=str, required=True, location="args") + args = parser.parse_args() + + try: + options = PluginParameterService.get_dynamic_select_options( + tenant_id, + user_id, + args["plugin_id"], + args["provider"], + args["action"], + args["parameter"], + args["provider_type"], + ) + except PluginDaemonClientSideError as e: + raise ValueError(e) + + return jsonable_encoder({"options": options}) + + api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginListApi, "/workspaces/current/plugin/list") api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") @@ -521,3 +558,5 @@ api.add_resource(PluginFetchMarketplacePkgApi, "/workspaces/current/plugin/marke api.add_resource(PluginChangePermissionApi, "/workspaces/current/plugin/permission/change") api.add_resource(PluginFetchPermissionApi, "/workspaces/current/plugin/permission/fetch") + +api.add_resource(PluginFetchDynamicSelectOptionsApi, "/workspaces/current/plugin/parameters/dynamic-options") diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index 2b1379bfb2..df50871a38 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -1,6 +1,7 @@ import io +from urllib.parse import urlparse -from flask import send_file +from flask import redirect, send_file from flask_login import current_user from flask_restful import Resource, reqparse from sqlalchemy.orm import Session @@ -9,17 +10,34 @@ from werkzeug.exceptions import Forbidden from configs import dify_config from controllers.console import api from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required +from core.mcp.auth.auth_flow import auth, handle_callback +from core.mcp.auth.auth_provider import OAuthClientProvider +from core.mcp.error import MCPAuthError, MCPError +from core.mcp.mcp_client import MCPClient from core.model_runtime.utils.encoders import jsonable_encoder from extensions.ext_database import db from libs.helper import alphanumeric, uuid_value from libs.login import login_required from services.tools.api_tools_manage_service import ApiToolManageService from services.tools.builtin_tools_manage_service import BuiltinToolManageService +from services.tools.mcp_tools_mange_service import MCPToolManageService from services.tools.tool_labels_service import ToolLabelsService from services.tools.tools_manage_service import ToolCommonService +from services.tools.tools_transform_service import ToolTransformService from services.tools.workflow_tools_manage_service import WorkflowToolManageService +def is_valid_url(url: str) -> bool: + if not url: + return False + + try: + parsed = urlparse(url) + return all([parsed.scheme, parsed.netloc]) and parsed.scheme in ["http", "https"] + except Exception: + return False + + class ToolProviderListApi(Resource): @setup_required @login_required @@ -34,7 +52,7 @@ class ToolProviderListApi(Resource): req.add_argument( "type", type=str, - choices=["builtin", "model", "api", "workflow"], + choices=["builtin", "model", "api", "workflow", "mcp"], required=False, nullable=True, location="args", @@ -613,6 +631,166 @@ class ToolLabelsApi(Resource): return jsonable_encoder(ToolLabelsService.list_tool_labels()) +class ToolProviderMCPApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("server_url", type=str, required=True, nullable=False, location="json") + parser.add_argument("name", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json", default="") + parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + user = current_user + if not is_valid_url(args["server_url"]): + raise ValueError("Server URL is not valid.") + return jsonable_encoder( + MCPToolManageService.create_mcp_provider( + tenant_id=user.current_tenant_id, + server_url=args["server_url"], + name=args["name"], + icon=args["icon"], + icon_type=args["icon_type"], + icon_background=args["icon_background"], + user_id=user.id, + server_identifier=args["server_identifier"], + ) + ) + + @setup_required + @login_required + @account_initialization_required + def put(self): + parser = reqparse.RequestParser() + parser.add_argument("server_url", type=str, required=True, nullable=False, location="json") + parser.add_argument("name", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json") + parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json") + parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json") + parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + if not is_valid_url(args["server_url"]): + if "[__HIDDEN__]" in args["server_url"]: + pass + else: + raise ValueError("Server URL is not valid.") + MCPToolManageService.update_mcp_provider( + tenant_id=current_user.current_tenant_id, + provider_id=args["provider_id"], + server_url=args["server_url"], + name=args["name"], + icon=args["icon"], + icon_type=args["icon_type"], + icon_background=args["icon_background"], + server_identifier=args["server_identifier"], + ) + return {"result": "success"} + + @setup_required + @login_required + @account_initialization_required + def delete(self): + parser = reqparse.RequestParser() + parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + MCPToolManageService.delete_mcp_tool(tenant_id=current_user.current_tenant_id, provider_id=args["provider_id"]) + return {"result": "success"} + + +class ToolMCPAuthApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json") + parser.add_argument("authorization_code", type=str, required=False, nullable=True, location="json") + args = parser.parse_args() + provider_id = args["provider_id"] + tenant_id = current_user.current_tenant_id + provider = MCPToolManageService.get_mcp_provider_by_provider_id(provider_id, tenant_id) + if not provider: + raise ValueError("provider not found") + try: + with MCPClient( + provider.decrypted_server_url, + provider_id, + tenant_id, + authed=False, + authorization_code=args["authorization_code"], + for_list=True, + ): + MCPToolManageService.update_mcp_provider_credentials( + mcp_provider=provider, + credentials=provider.decrypted_credentials, + authed=True, + ) + return {"result": "success"} + + except MCPAuthError: + auth_provider = OAuthClientProvider(provider_id, tenant_id, for_list=True) + return auth(auth_provider, provider.decrypted_server_url, args["authorization_code"]) + except MCPError as e: + MCPToolManageService.update_mcp_provider_credentials( + mcp_provider=provider, + credentials={}, + authed=False, + ) + raise ValueError(f"Failed to connect to MCP server: {e}") from e + + +class ToolMCPDetailApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider_id): + user = current_user + provider = MCPToolManageService.get_mcp_provider_by_provider_id(provider_id, user.current_tenant_id) + return jsonable_encoder(ToolTransformService.mcp_provider_to_user_provider(provider, for_list=True)) + + +class ToolMCPListAllApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self): + user = current_user + tenant_id = user.current_tenant_id + + tools = MCPToolManageService.retrieve_mcp_tools(tenant_id=tenant_id) + + return [tool.to_dict() for tool in tools] + + +class ToolMCPUpdateApi(Resource): + @setup_required + @login_required + @account_initialization_required + def get(self, provider_id): + tenant_id = current_user.current_tenant_id + tools = MCPToolManageService.list_mcp_tool_from_remote_server( + tenant_id=tenant_id, + provider_id=provider_id, + ) + return jsonable_encoder(tools) + + +class ToolMCPCallbackApi(Resource): + def get(self): + parser = reqparse.RequestParser() + parser.add_argument("code", type=str, required=True, nullable=False, location="args") + parser.add_argument("state", type=str, required=True, nullable=False, location="args") + args = parser.parse_args() + state_key = args["state"] + authorization_code = args["code"] + handle_callback(state_key, authorization_code) + return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback") + + # tool provider api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers") @@ -647,8 +825,15 @@ api.add_resource(ToolWorkflowProviderDeleteApi, "/workspaces/current/tool-provid api.add_resource(ToolWorkflowProviderGetApi, "/workspaces/current/tool-provider/workflow/get") api.add_resource(ToolWorkflowProviderListToolApi, "/workspaces/current/tool-provider/workflow/tools") +# mcp tool provider +api.add_resource(ToolMCPDetailApi, "/workspaces/current/tool-provider/mcp/tools/") +api.add_resource(ToolProviderMCPApi, "/workspaces/current/tool-provider/mcp") +api.add_resource(ToolMCPUpdateApi, "/workspaces/current/tool-provider/mcp/update/") +api.add_resource(ToolMCPAuthApi, "/workspaces/current/tool-provider/mcp/auth") +api.add_resource(ToolMCPCallbackApi, "/mcp/oauth/callback") + api.add_resource(ToolBuiltinListApi, "/workspaces/current/tools/builtin") api.add_resource(ToolApiListApi, "/workspaces/current/tools/api") +api.add_resource(ToolMCPListAllApi, "/workspaces/current/tools/mcp") api.add_resource(ToolWorkflowListApi, "/workspaces/current/tools/workflow") - api.add_resource(ToolLabelsApi, "/workspaces/current/tool-labels") diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index 360cbd9246..ca122772de 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -44,6 +44,17 @@ def only_edition_cloud(view): return decorated +def only_edition_enterprise(view): + @wraps(view) + def decorated(*args, **kwargs): + if not dify_config.ENTERPRISE_ENABLED: + abort(404) + + return view(*args, **kwargs) + + return decorated + + def only_edition_self_hosted(view): @wraps(view) def decorated(*args, **kwargs): diff --git a/api/controllers/files/upload.py b/api/controllers/files/upload.py index f1a15793c7..15f93d2774 100644 --- a/api/controllers/files/upload.py +++ b/api/controllers/files/upload.py @@ -87,7 +87,5 @@ class PluginUploadFileApi(Resource): except services.errors.file.UnsupportedFileTypeError: raise UnsupportedFileTypeError() - return tool_file, 201 - api.add_resource(PluginUploadFileApi, "/files/upload/for-plugin") diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py index f3a1bd8fa5..327e9ce834 100644 --- a/api/controllers/inner_api/plugin/plugin.py +++ b/api/controllers/inner_api/plugin/plugin.py @@ -17,6 +17,7 @@ from core.plugin.entities.request import ( RequestInvokeApp, RequestInvokeEncrypt, RequestInvokeLLM, + RequestInvokeLLMWithStructuredOutput, RequestInvokeModeration, RequestInvokeParameterExtractorNode, RequestInvokeQuestionClassifierNode, @@ -29,7 +30,7 @@ from core.plugin.entities.request import ( RequestRequestUploadFile, ) from core.tools.entities.tool_entities import ToolProviderType -from libs.helper import compact_generate_response +from libs.helper import length_prefixed_response from models.account import Account, Tenant from models.model import EndUser @@ -44,7 +45,22 @@ class PluginInvokeLLMApi(Resource): response = PluginModelBackwardsInvocation.invoke_llm(user_model.id, tenant_model, payload) return PluginModelBackwardsInvocation.convert_to_event_stream(response) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) + + +class PluginInvokeLLMWithStructuredOutputApi(Resource): + @setup_required + @plugin_inner_api_only + @get_user_tenant + @plugin_data(payload_type=RequestInvokeLLMWithStructuredOutput) + def post(self, user_model: Account | EndUser, tenant_model: Tenant, payload: RequestInvokeLLMWithStructuredOutput): + def generator(): + response = PluginModelBackwardsInvocation.invoke_llm_with_structured_output( + user_model.id, tenant_model, payload + ) + return PluginModelBackwardsInvocation.convert_to_event_stream(response) + + return length_prefixed_response(0xF, generator()) class PluginInvokeTextEmbeddingApi(Resource): @@ -101,7 +117,7 @@ class PluginInvokeTTSApi(Resource): ) return PluginModelBackwardsInvocation.convert_to_event_stream(response) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) class PluginInvokeSpeech2TextApi(Resource): @@ -162,7 +178,7 @@ class PluginInvokeToolApi(Resource): ), ) - return compact_generate_response(generator()) + return length_prefixed_response(0xF, generator()) class PluginInvokeParameterExtractorNodeApi(Resource): @@ -228,7 +244,7 @@ class PluginInvokeAppApi(Resource): files=payload.files, ) - return compact_generate_response(PluginAppBackwardsInvocation.convert_to_event_stream(response)) + return length_prefixed_response(0xF, PluginAppBackwardsInvocation.convert_to_event_stream(response)) class PluginInvokeEncryptApi(Resource): @@ -291,6 +307,7 @@ class PluginFetchAppInfoApi(Resource): api.add_resource(PluginInvokeLLMApi, "/invoke/llm") +api.add_resource(PluginInvokeLLMWithStructuredOutputApi, "/invoke/llm/structured-output") api.add_resource(PluginInvokeTextEmbeddingApi, "/invoke/text-embedding") api.add_resource(PluginInvokeRerankApi, "/invoke/rerank") api.add_resource(PluginInvokeTTSApi, "/invoke/tts") diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index b2849a7962..50408e0929 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -32,6 +32,7 @@ def get_user(tenant_id: str, user_id: str | None) -> Account | EndUser: ) session.add(user_model) session.commit() + session.refresh(user_model) else: user_model = AccountService.load_user(user_id) if not user_model: diff --git a/api/controllers/inner_api/workspace/workspace.py b/api/controllers/inner_api/workspace/workspace.py index a2fc2d4675..77568b75f1 100644 --- a/api/controllers/inner_api/workspace/workspace.py +++ b/api/controllers/inner_api/workspace/workspace.py @@ -29,7 +29,19 @@ class EnterpriseWorkspace(Resource): tenant_was_created.send(tenant) - return {"message": "enterprise workspace created."} + resp = { + "id": tenant.id, + "name": tenant.name, + "plan": tenant.plan, + "status": tenant.status, + "created_at": tenant.created_at.isoformat() + "Z" if tenant.created_at else None, + "updated_at": tenant.updated_at.isoformat() + "Z" if tenant.updated_at else None, + } + + return { + "message": "enterprise workspace created.", + "tenant": resp, + } class EnterpriseWorkspaceNoOwnerEmail(Resource): diff --git a/api/controllers/mcp/__init__.py b/api/controllers/mcp/__init__.py new file mode 100644 index 0000000000..1b3e0a5621 --- /dev/null +++ b/api/controllers/mcp/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + +from libs.external_api import ExternalApi + +bp = Blueprint("mcp", __name__, url_prefix="/mcp") +api = ExternalApi(bp) + +from . import mcp diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py new file mode 100644 index 0000000000..ead728bfb0 --- /dev/null +++ b/api/controllers/mcp/mcp.py @@ -0,0 +1,104 @@ +from flask_restful import Resource, reqparse +from pydantic import ValidationError + +from controllers.console.app.mcp_server import AppMCPServerStatus +from controllers.mcp import api +from core.app.app_config.entities import VariableEntity +from core.mcp import types +from core.mcp.server.streamable_http import MCPServerStreamableHTTPRequestHandler +from core.mcp.types import ClientNotification, ClientRequest +from core.mcp.utils import create_mcp_error_response +from extensions.ext_database import db +from libs import helper +from models.model import App, AppMCPServer, AppMode + + +class MCPAppApi(Resource): + def post(self, server_code): + def int_or_str(value): + if isinstance(value, (int, str)): + return value + else: + return None + + parser = reqparse.RequestParser() + parser.add_argument("jsonrpc", type=str, required=True, location="json") + parser.add_argument("method", type=str, required=True, location="json") + parser.add_argument("params", type=dict, required=False, location="json") + parser.add_argument("id", type=int_or_str, required=False, location="json") + args = parser.parse_args() + + request_id = args.get("id") + + server = db.session.query(AppMCPServer).filter(AppMCPServer.server_code == server_code).first() + if not server: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server Not Found") + ) + + if server.status != AppMCPServerStatus.ACTIVE: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_REQUEST, "Server is not active") + ) + + app = db.session.query(App).filter(App.id == server.app_id).first() + if not app: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_REQUEST, "App Not Found") + ) + + if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: + workflow = app.workflow + if workflow is None: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable") + ) + + user_input_form = workflow.user_input_form(to_old_structure=True) + else: + app_model_config = app.app_model_config + if app_model_config is None: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_REQUEST, "App is unavailable") + ) + + features_dict = app_model_config.to_dict() + user_input_form = features_dict.get("user_input_form", []) + converted_user_input_form: list[VariableEntity] = [] + try: + for item in user_input_form: + variable_type = item.get("type", "") or list(item.keys())[0] + variable = item[variable_type] + converted_user_input_form.append( + VariableEntity( + type=variable_type, + variable=variable.get("variable"), + description=variable.get("description") or "", + label=variable.get("label"), + required=variable.get("required", False), + max_length=variable.get("max_length"), + options=variable.get("options") or [], + ) + ) + except ValidationError as e: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid user_input_form: {str(e)}") + ) + + try: + request: ClientRequest | ClientNotification = ClientRequest.model_validate(args) + except ValidationError as e: + try: + notification = ClientNotification.model_validate(args) + request = notification + except ValidationError as e: + return helper.compact_generate_response( + create_mcp_error_response(request_id, types.INVALID_PARAMS, f"Invalid MCP request: {str(e)}") + ) + + mcp_server_handler = MCPServerStreamableHTTPRequestHandler(app, request, converted_user_input_form) + response = mcp_server_handler.handle() + return helper.compact_generate_response(response) + + +api.add_resource(MCPAppApi, "/server//mcp") diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index 2c03aba33d..89222d5e83 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -47,7 +47,13 @@ class AppInfoApi(Resource): def get(self, app_model: App): """Get app information""" tags = [tag.name for tag in app_model.tags] - return {"name": app_model.name, "description": app_model.description, "tags": tags, "mode": app_model.mode} + return { + "name": app_model.name, + "description": app_model.description, + "tags": tags, + "mode": app_model.mode, + "author_name": app_model.author_name, + } api.add_resource(AppParameterApi, "/parameters") diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index 2682c2e7f1..848863cf1b 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -20,7 +20,7 @@ from controllers.service_api.app.error import ( from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError -from models.model import App, AppMode, EndUser +from models.model import App, EndUser from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, @@ -78,20 +78,9 @@ class TextApi(Resource): message_id = args.get("message_id", None) text = args.get("text", None) - if ( - app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value} - and app_model.workflow - and app_model.workflow.features_dict - ): - text_to_speech = app_model.workflow.features_dict.get("text_to_speech", {}) - voice = args.get("voice") or text_to_speech.get("voice") - else: - try: - voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") - except Exception: - voice = None + voice = args.get("voice", None) response = AudioService.transcript_tts( - app_model=app_model, message_id=message_id, end_user=end_user.external_user_id, voice=voice, text=text + app_model=app_model, text=text, voice=voice, end_user=end_user.external_user_id, message_id=message_id ) return response diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index df52b49424..ac2ebf2b09 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -3,7 +3,7 @@ import logging from dateutil.parser import isoparse from flask_restful import Resource, fields, marshal_with, reqparse from flask_restful.inputs import int_range -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import InternalServerError from controllers.service_api import api @@ -30,7 +30,7 @@ from fields.workflow_app_log_fields import workflow_app_log_pagination_fields from libs import helper from libs.helper import TimestampField from models.model import App, AppMode, EndUser -from models.workflow import WorkflowRun +from repositories.factory import DifyAPIRepositoryFactory from services.app_generate_service import AppGenerateService from services.errors.llm import InvokeRateLimitError from services.workflow_app_service import WorkflowAppService @@ -63,7 +63,15 @@ class WorkflowRunDetailApi(Resource): if app_mode not in [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]: raise NotWorkflowAppError() - workflow_run = db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).first() + # Use repository to get workflow run + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + + workflow_run = workflow_run_repo.get_workflow_run_by_id( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + run_id=workflow_run_id, + ) return workflow_run @@ -135,6 +143,20 @@ class WorkflowAppLogApi(Resource): parser.add_argument("status", type=str, choices=["succeeded", "failed", "stopped"], location="args") parser.add_argument("created_at__before", type=str, location="args") parser.add_argument("created_at__after", type=str, location="args") + parser.add_argument( + "created_by_end_user_session_id", + type=str, + location="args", + required=False, + default=None, + ) + parser.add_argument( + "created_by_account", + type=str, + location="args", + required=False, + default=None, + ) parser.add_argument("page", type=int_range(1, 99999), default=1, location="args") parser.add_argument("limit", type=int_range(1, 100), default=20, location="args") args = parser.parse_args() @@ -158,6 +180,8 @@ class WorkflowAppLogApi(Resource): created_at_after=args.created_at__after, page=args.page, limit=args.limit, + created_by_end_user_session_id=args.created_by_end_user_session_id, + created_by_account=args.created_by_account, ) return workflow_app_log_pagination diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index 27e8dd3fa6..a499719fc3 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -4,8 +4,12 @@ from werkzeug.exceptions import Forbidden, NotFound import services.dataset_service from controllers.service_api import api -from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError -from controllers.service_api.wraps import DatasetApiResource, validate_dataset_token +from controllers.service_api.dataset.error import DatasetInUseError, DatasetNameDuplicateError, InvalidActionError +from controllers.service_api.wraps import ( + DatasetApiResource, + cloud_edition_billing_rate_limit_check, + validate_dataset_token, +) from core.model_runtime.entities.model_entities import ModelType from core.plugin.entities.plugin import ModelProviderID from core.provider_manager import ProviderManager @@ -13,7 +17,7 @@ from fields.dataset_fields import dataset_detail_fields from fields.tag_fields import tag_fields from libs.login import current_user from models.dataset import Dataset, DatasetPermissionEnum -from services.dataset_service import DatasetPermissionService, DatasetService +from services.dataset_service import DatasetPermissionService, DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import RetrievalModel from services.tag_service import TagService @@ -70,6 +74,7 @@ class DatasetListApi(DatasetApiResource): response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page} return response, 200 + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id): """Resource for creating datasets.""" parser = reqparse.RequestParser() @@ -128,6 +133,22 @@ class DatasetListApi(DatasetApiResource): parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json") args = parser.parse_args() + + if args.get("embedding_model_provider"): + DatasetService.check_embedding_model_setting( + tenant_id, args.get("embedding_model_provider"), args.get("embedding_model") + ) + if ( + args.get("retrieval_model") + and args.get("retrieval_model").get("reranking_model") + and args.get("retrieval_model").get("reranking_model").get("reranking_provider_name") + ): + DatasetService.check_reranking_model_setting( + tenant_id, + args.get("retrieval_model").get("reranking_model").get("reranking_provider_name"), + args.get("retrieval_model").get("reranking_model").get("reranking_model_name"), + ) + try: dataset = DatasetService.create_empty_dataset( tenant_id=tenant_id, @@ -193,6 +214,7 @@ class DatasetApi(DatasetApiResource): return data, 200 + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def patch(self, _, dataset_id): dataset_id_str = str(dataset_id) dataset = DatasetService.get_dataset(dataset_id_str) @@ -259,10 +281,20 @@ class DatasetApi(DatasetApiResource): data = request.get_json() # check embedding model setting - if data.get("indexing_technique") == "high_quality": + if data.get("indexing_technique") == "high_quality" or data.get("embedding_model_provider"): DatasetService.check_embedding_model_setting( dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model") ) + if ( + data.get("retrieval_model") + and data.get("retrieval_model").get("reranking_model") + and data.get("retrieval_model").get("reranking_model").get("reranking_provider_name") + ): + DatasetService.check_reranking_model_setting( + dataset.tenant_id, + data.get("retrieval_model").get("reranking_model").get("reranking_provider_name"), + data.get("retrieval_model").get("reranking_model").get("reranking_model_name"), + ) # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator DatasetPermissionService.check_permission( @@ -293,6 +325,7 @@ class DatasetApi(DatasetApiResource): return result_data, 200 + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def delete(self, _, dataset_id): """ Deletes a dataset given its ID. @@ -322,6 +355,56 @@ class DatasetApi(DatasetApiResource): raise DatasetInUseError() +class DocumentStatusApi(DatasetApiResource): + """Resource for batch document status operations.""" + + def patch(self, tenant_id, dataset_id, action): + """ + Batch update document status. + + Args: + tenant_id: tenant id + dataset_id: dataset id + action: action to perform (enable, disable, archive, un_archive) + + Returns: + dict: A dictionary with a key 'result' and a value 'success' + int: HTTP status code 200 indicating that the operation was successful. + + Raises: + NotFound: If the dataset with the given ID does not exist. + Forbidden: If the user does not have permission. + InvalidActionError: If the action is invalid or cannot be performed. + """ + dataset_id_str = str(dataset_id) + dataset = DatasetService.get_dataset(dataset_id_str) + + if dataset is None: + raise NotFound("Dataset not found.") + + # Check user's permission + try: + DatasetService.check_dataset_permission(dataset, current_user) + except services.errors.account.NoPermissionError as e: + raise Forbidden(str(e)) + + # Check dataset model setting + DatasetService.check_dataset_model_setting(dataset) + + # Get document IDs from request body + data = request.get_json() + document_ids = data.get("document_ids", []) + + try: + DocumentService.batch_update_document_status(dataset, document_ids, action, current_user) + except services.errors.document.DocumentIndexingError as e: + raise InvalidActionError(str(e)) + except ValueError as e: + raise InvalidActionError(str(e)) + + return {"result": "success"}, 200 + + class DatasetTagsApi(DatasetApiResource): @validate_dataset_token @marshal_with(tag_fields) @@ -450,6 +533,7 @@ class DatasetTagsBindingStatusApi(DatasetApiResource): api.add_resource(DatasetListApi, "/datasets") api.add_resource(DatasetApi, "/datasets/") +api.add_resource(DocumentStatusApi, "/datasets//documents/status/") api.add_resource(DatasetTagsApi, "/datasets/tags") api.add_resource(DatasetTagBindingApi, "/datasets/tags/binding") api.add_resource(DatasetTagUnbindingApi, "/datasets/tags/unbinding") diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index ab7ab4dcf0..d571b21a0a 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -3,7 +3,7 @@ import json from flask import request from flask_restful import marshal, reqparse from sqlalchemy import desc, select -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import Forbidden, NotFound import services from controllers.common.errors import FilenameNotExistsError @@ -18,14 +18,19 @@ from controllers.service_api.app.error import ( from controllers.service_api.dataset.error import ( ArchivedDocumentImmutableError, DocumentIndexingError, + InvalidMetadataError, +) +from controllers.service_api.wraps import ( + DatasetApiResource, + cloud_edition_billing_rate_limit_check, + cloud_edition_billing_resource_check, ) -from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check from core.errors.error import ProviderTokenNotInitError from extensions.ext_database import db from fields.document_fields import document_fields, document_status_fields from libs.login import current_user from models.dataset import Dataset, Document, DocumentSegment -from services.dataset_service import DocumentService +from services.dataset_service import DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig from services.file_service import FileService @@ -35,6 +40,7 @@ class DocumentAddByTextApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("documents", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id): """Create document by text.""" parser = reqparse.RequestParser() @@ -54,6 +60,7 @@ class DocumentAddByTextApi(DatasetApiResource): parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json") args = parser.parse_args() + dataset_id = str(dataset_id) tenant_id = str(tenant_id) dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first() @@ -69,6 +76,21 @@ class DocumentAddByTextApi(DatasetApiResource): if text is None or name is None: raise ValueError("Both 'text' and 'name' must be non-null values.") + if args.get("embedding_model_provider"): + DatasetService.check_embedding_model_setting( + tenant_id, args.get("embedding_model_provider"), args.get("embedding_model") + ) + if ( + args.get("retrieval_model") + and args.get("retrieval_model").get("reranking_model") + and args.get("retrieval_model").get("reranking_model").get("reranking_provider_name") + ): + DatasetService.check_reranking_model_setting( + tenant_id, + args.get("retrieval_model").get("reranking_model").get("reranking_provider_name"), + args.get("retrieval_model").get("reranking_model").get("reranking_model_name"), + ) + upload_file = FileService.upload_text(text=str(text), text_name=str(name)) data_source = { "type": "upload_file", @@ -99,6 +121,7 @@ class DocumentUpdateByTextApi(DatasetApiResource): """Resource for update documents.""" @cloud_edition_billing_resource_check("vector_space", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, document_id): """Update document by text.""" parser = reqparse.RequestParser() @@ -118,6 +141,17 @@ class DocumentUpdateByTextApi(DatasetApiResource): if not dataset: raise ValueError("Dataset does not exist.") + if ( + args.get("retrieval_model") + and args.get("retrieval_model").get("reranking_model") + and args.get("retrieval_model").get("reranking_model").get("reranking_provider_name") + ): + DatasetService.check_reranking_model_setting( + tenant_id, + args.get("retrieval_model").get("reranking_model").get("reranking_provider_name"), + args.get("retrieval_model").get("reranking_model").get("reranking_model_name"), + ) + # indexing_technique is already set in dataset since this is an update args["indexing_technique"] = dataset.indexing_technique @@ -158,6 +192,7 @@ class DocumentAddByFileApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("documents", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id): """Create document by upload file.""" args = {} @@ -176,11 +211,29 @@ class DocumentAddByFileApi(DatasetApiResource): if not dataset: raise ValueError("Dataset does not exist.") + if dataset.provider == "external": + raise ValueError("External datasets are not supported.") + indexing_technique = args.get("indexing_technique") or dataset.indexing_technique if not indexing_technique: raise ValueError("indexing_technique is required.") args["indexing_technique"] = indexing_technique + if "embedding_model_provider" in args: + DatasetService.check_embedding_model_setting( + tenant_id, args["embedding_model_provider"], args["embedding_model"] + ) + if ( + "retrieval_model" in args + and args["retrieval_model"].get("reranking_model") + and args["retrieval_model"].get("reranking_model").get("reranking_provider_name") + ): + DatasetService.check_reranking_model_setting( + tenant_id, + args["retrieval_model"].get("reranking_model").get("reranking_provider_name"), + args["retrieval_model"].get("reranking_model").get("reranking_model_name"), + ) + # save file info file = request.files["file"] # check file @@ -232,6 +285,7 @@ class DocumentUpdateByFileApi(DatasetApiResource): """Resource for update documents.""" @cloud_edition_billing_resource_check("vector_space", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, document_id): """Update document by upload file.""" args = {} @@ -250,6 +304,9 @@ class DocumentUpdateByFileApi(DatasetApiResource): if not dataset: raise ValueError("Dataset does not exist.") + if dataset.provider == "external": + raise ValueError("External datasets are not supported.") + # indexing_technique is already set in dataset since this is an update args["indexing_technique"] = dataset.indexing_technique @@ -302,6 +359,7 @@ class DocumentUpdateByFileApi(DatasetApiResource): class DocumentDeleteApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def delete(self, tenant_id, dataset_id, document_id): """Delete document.""" document_id = str(document_id) @@ -415,6 +473,101 @@ class DocumentIndexingStatusApi(DatasetApiResource): return data +class DocumentDetailApi(DatasetApiResource): + METADATA_CHOICES = {"all", "only", "without"} + + def get(self, tenant_id, dataset_id, document_id): + dataset_id = str(dataset_id) + document_id = str(document_id) + + dataset = self.get_dataset(dataset_id, tenant_id) + + document = DocumentService.get_document(dataset.id, document_id) + + if not document: + raise NotFound("Document not found.") + + if document.tenant_id != str(tenant_id): + raise Forbidden("No permission.") + + metadata = request.args.get("metadata", "all") + if metadata not in self.METADATA_CHOICES: + raise InvalidMetadataError(f"Invalid metadata value: {metadata}") + + if metadata == "only": + response = {"id": document.id, "doc_type": document.doc_type, "doc_metadata": document.doc_metadata_details} + elif metadata == "without": + dataset_process_rules = DatasetService.get_process_rules(dataset_id) + document_process_rules = document.dataset_process_rule.to_dict() + data_source_info = document.data_source_detail_dict + response = { + "id": document.id, + "position": document.position, + "data_source_type": document.data_source_type, + "data_source_info": data_source_info, + "dataset_process_rule_id": document.dataset_process_rule_id, + "dataset_process_rule": dataset_process_rules, + "document_process_rule": document_process_rules, + "name": document.name, + "created_from": document.created_from, + "created_by": document.created_by, + "created_at": document.created_at.timestamp(), + "tokens": document.tokens, + "indexing_status": document.indexing_status, + "completed_at": int(document.completed_at.timestamp()) if document.completed_at else None, + "updated_at": int(document.updated_at.timestamp()) if document.updated_at else None, + "indexing_latency": document.indexing_latency, + "error": document.error, + "enabled": document.enabled, + "disabled_at": int(document.disabled_at.timestamp()) if document.disabled_at else None, + "disabled_by": document.disabled_by, + "archived": document.archived, + "segment_count": document.segment_count, + "average_segment_length": document.average_segment_length, + "hit_count": document.hit_count, + "display_status": document.display_status, + "doc_form": document.doc_form, + "doc_language": document.doc_language, + } + else: + dataset_process_rules = DatasetService.get_process_rules(dataset_id) + document_process_rules = document.dataset_process_rule.to_dict() + data_source_info = document.data_source_detail_dict + response = { + "id": document.id, + "position": document.position, + "data_source_type": document.data_source_type, + "data_source_info": data_source_info, + "dataset_process_rule_id": document.dataset_process_rule_id, + "dataset_process_rule": dataset_process_rules, + "document_process_rule": document_process_rules, + "name": document.name, + "created_from": document.created_from, + "created_by": document.created_by, + "created_at": document.created_at.timestamp(), + "tokens": document.tokens, + "indexing_status": document.indexing_status, + "completed_at": int(document.completed_at.timestamp()) if document.completed_at else None, + "updated_at": int(document.updated_at.timestamp()) if document.updated_at else None, + "indexing_latency": document.indexing_latency, + "error": document.error, + "enabled": document.enabled, + "disabled_at": int(document.disabled_at.timestamp()) if document.disabled_at else None, + "disabled_by": document.disabled_by, + "archived": document.archived, + "doc_type": document.doc_type, + "doc_metadata": document.doc_metadata_details, + "segment_count": document.segment_count, + "average_segment_length": document.average_segment_length, + "hit_count": document.hit_count, + "display_status": document.display_status, + "doc_form": document.doc_form, + "doc_language": document.doc_language, + } + + return response + + api.add_resource( DocumentAddByTextApi, "/datasets//document/create_by_text", @@ -438,3 +591,4 @@ api.add_resource( api.add_resource(DocumentDeleteApi, "/datasets//documents/") api.add_resource(DocumentListApi, "/datasets//documents") api.add_resource(DocumentIndexingStatusApi, "/datasets//documents//indexing-status") +api.add_resource(DocumentDetailApi, "/datasets//documents/") diff --git a/api/controllers/service_api/dataset/hit_testing.py b/api/controllers/service_api/dataset/hit_testing.py index 465f71bf03..52e9bca5da 100644 --- a/api/controllers/service_api/dataset/hit_testing.py +++ b/api/controllers/service_api/dataset/hit_testing.py @@ -1,9 +1,10 @@ from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase from controllers.service_api import api -from controllers.service_api.wraps import DatasetApiResource +from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check class HitTestingApi(DatasetApiResource, DatasetsHitTestingBase): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id): dataset_id_str = str(dataset_id) diff --git a/api/controllers/service_api/dataset/metadata.py b/api/controllers/service_api/dataset/metadata.py index 35582feea0..1968696ee5 100644 --- a/api/controllers/service_api/dataset/metadata.py +++ b/api/controllers/service_api/dataset/metadata.py @@ -3,7 +3,7 @@ from flask_restful import marshal, reqparse from werkzeug.exceptions import NotFound from controllers.service_api import api -from controllers.service_api.wraps import DatasetApiResource +from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_rate_limit_check from fields.dataset_fields import dataset_metadata_fields from services.dataset_service import DatasetService from services.entities.knowledge_entities.knowledge_entities import ( @@ -14,6 +14,7 @@ from services.metadata_service import MetadataService class DatasetMetadataCreateServiceApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id): parser = reqparse.RequestParser() parser.add_argument("type", type=str, required=True, nullable=True, location="json") @@ -39,6 +40,7 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource): class DatasetMetadataServiceApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def patch(self, tenant_id, dataset_id, metadata_id): parser = reqparse.RequestParser() parser.add_argument("name", type=str, required=True, nullable=True, location="json") @@ -54,6 +56,7 @@ class DatasetMetadataServiceApi(DatasetApiResource): metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, args.get("name")) return marshal(metadata, dataset_metadata_fields), 200 + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def delete(self, tenant_id, dataset_id, metadata_id): dataset_id_str = str(dataset_id) metadata_id_str = str(metadata_id) @@ -73,6 +76,7 @@ class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource): class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, action): dataset_id_str = str(dataset_id) dataset = DatasetService.get_dataset(dataset_id_str) @@ -88,6 +92,7 @@ class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): class DocumentMetadataEditServiceApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id): dataset_id_str = str(dataset_id) dataset = DatasetService.get_dataset(dataset_id_str) diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index 337752275a..403b7f0a0c 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -8,6 +8,7 @@ from controllers.service_api.app.error import ProviderNotInitializeError from controllers.service_api.wraps import ( DatasetApiResource, cloud_edition_billing_knowledge_limit_check, + cloud_edition_billing_rate_limit_check, cloud_edition_billing_resource_check, ) from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError @@ -35,6 +36,7 @@ class SegmentApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, document_id): """Create single segment.""" # check dataset @@ -139,6 +141,7 @@ class SegmentApi(DatasetApiResource): class DatasetSegmentApi(DatasetApiResource): + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def delete(self, tenant_id, dataset_id, document_id, segment_id): # check dataset dataset_id = str(dataset_id) @@ -162,6 +165,7 @@ class DatasetSegmentApi(DatasetApiResource): return 204 @cloud_edition_billing_resource_check("vector_space", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, document_id, segment_id): # check dataset dataset_id = str(dataset_id) @@ -236,6 +240,7 @@ class ChildChunkApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id, document_id, segment_id): """Create child chunk.""" # check dataset @@ -332,6 +337,7 @@ class DatasetChildChunkApi(DatasetApiResource): """Resource for updating child chunks.""" @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def delete(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id): """Delete child chunk.""" # check dataset @@ -370,6 +376,7 @@ class DatasetChildChunkApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def patch(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id): """Update child chunk.""" # check dataset diff --git a/api/controllers/service_api/index.py b/api/controllers/service_api/index.py index d24c4597e2..9bb5df4c4e 100644 --- a/api/controllers/service_api/index.py +++ b/api/controllers/service_api/index.py @@ -9,7 +9,7 @@ class IndexApi(Resource): return { "welcome": "Dify OpenAPI", "api_version": "v1", - "server_version": dify_config.CURRENT_VERSION, + "server_version": dify_config.project.version, } diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index d3316a5159..5b919a68d4 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -11,13 +11,13 @@ from flask_restful import Resource from pydantic import BaseModel from sqlalchemy import select, update from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden, Unauthorized +from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from extensions.ext_database import db from extensions.ext_redis import redis_client from libs.login import _get_user from models.account import Account, Tenant, TenantAccountJoin, TenantStatus -from models.dataset import RateLimitLog +from models.dataset import Dataset, RateLimitLog from models.model import ApiToken, App, EndUser from services.feature_service import FeatureService @@ -317,3 +317,11 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str] class DatasetApiResource(Resource): method_decorators = [validate_dataset_token] + + def get_dataset(self, dataset_id: str, tenant_id: str) -> Dataset: + dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id, Dataset.tenant_id == tenant_id).first() + + if not dataset: + raise NotFound("Dataset not found.") + + return dataset diff --git a/api/controllers/web/__init__.py b/api/controllers/web/__init__.py index 50a04a6254..56749a0e25 100644 --- a/api/controllers/web/__init__.py +++ b/api/controllers/web/__init__.py @@ -15,4 +15,17 @@ api.add_resource(FileApi, "/files/upload") api.add_resource(RemoteFileInfoApi, "/remote-files/") api.add_resource(RemoteFileUploadApi, "/remote-files/upload") -from . import app, audio, completion, conversation, feature, message, passport, saved_message, site, workflow +from . import ( + app, + audio, + completion, + conversation, + feature, + forgot_password, + login, + message, + passport, + saved_message, + site, + workflow, +) diff --git a/api/controllers/web/app.py b/api/controllers/web/app.py index bb4486bd91..94a525a75d 100644 --- a/api/controllers/web/app.py +++ b/api/controllers/web/app.py @@ -10,6 +10,8 @@ from libs.passport import PassportService from models.model import App, AppMode from services.app_service import AppService from services.enterprise.enterprise_service import EnterpriseService +from services.feature_service import FeatureService +from services.webapp_auth_service import WebAppAuthService class AppParameterApi(WebApiResource): @@ -46,10 +48,22 @@ class AppMeta(WebApiResource): class AppAccessMode(Resource): def get(self): parser = reqparse.RequestParser() - parser.add_argument("appId", type=str, required=True, location="args") + parser.add_argument("appId", type=str, required=False, location="args") + parser.add_argument("appCode", type=str, required=False, location="args") args = parser.parse_args() - app_id = args["appId"] + features = FeatureService.get_system_features() + if not features.webapp_auth.enabled: + return {"accessMode": "public"} + + app_id = args.get("appId") + if args.get("appCode"): + app_code = args["appCode"] + app_id = AppService.get_app_id_by_code(app_code) + + if not app_id: + raise ValueError("appId or appCode must be provided") + res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id) return {"accessMode": res.access_mode} @@ -75,6 +89,10 @@ class AppWebAuthPermission(Resource): except Exception as e: pass + features = FeatureService.get_system_features() + if not features.webapp_auth.enabled: + return {"result": True} + parser = reqparse.RequestParser() parser.add_argument("appId", type=str, required=True, location="args") args = parser.parse_args() @@ -82,7 +100,9 @@ class AppWebAuthPermission(Resource): app_id = args["appId"] app_code = AppService.get_app_code_by_id(app_id) - res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code) + res = True + if WebAppAuthService.is_app_require_permission_check(app_id=app_id): + res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code) return {"result": res} diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py index 06d9ad7564..2919ca9af4 100644 --- a/api/controllers/web/audio.py +++ b/api/controllers/web/audio.py @@ -19,7 +19,7 @@ from controllers.web.error import ( from controllers.web.wraps import WebApiResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError -from models.model import App, AppMode +from models.model import App from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, @@ -77,21 +77,9 @@ class TextApi(WebApiResource): message_id = args.get("message_id", None) text = args.get("text", None) - if ( - app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value} - and app_model.workflow - and app_model.workflow.features_dict - ): - text_to_speech = app_model.workflow.features_dict.get("text_to_speech", {}) - voice = args.get("voice") or text_to_speech.get("voice") - else: - try: - voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") - except Exception: - voice = None - + voice = args.get("voice", None) response = AudioService.transcript_tts( - app_model=app_model, message_id=message_id, end_user=end_user.external_user_id, voice=voice, text=text + app_model=app_model, text=text, voice=voice, end_user=end_user.external_user_id, message_id=message_id ) return response diff --git a/api/controllers/web/error.py b/api/controllers/web/error.py index 4371e679db..036e11d5c5 100644 --- a/api/controllers/web/error.py +++ b/api/controllers/web/error.py @@ -139,3 +139,13 @@ class InvokeRateLimitError(BaseHTTPException): error_code = "rate_limit_error" description = "Rate Limit Error" code = 429 + + +class NotFoundError(BaseHTTPException): + error_code = "not_found" + code = 404 + + +class InvalidArgumentError(BaseHTTPException): + error_code = "invalid_param" + code = 400 diff --git a/api/controllers/web/forgot_password.py b/api/controllers/web/forgot_password.py new file mode 100644 index 0000000000..0da8d65efc --- /dev/null +++ b/api/controllers/web/forgot_password.py @@ -0,0 +1,147 @@ +import base64 +import secrets + +from flask import request +from flask_restful import Resource, reqparse +from sqlalchemy import select +from sqlalchemy.orm import Session + +from controllers.console.auth.error import ( + EmailCodeError, + EmailPasswordResetLimitError, + InvalidEmailError, + InvalidTokenError, + PasswordMismatchError, +) +from controllers.console.error import AccountNotFound, EmailSendIpLimitError +from controllers.console.wraps import email_password_login_enabled, only_edition_enterprise, setup_required +from controllers.web import api +from extensions.ext_database import db +from libs.helper import email, extract_remote_ip +from libs.password import hash_password, valid_password +from models.account import Account +from services.account_service import AccountService + + +class ForgotPasswordSendEmailApi(Resource): + @only_edition_enterprise + @setup_required + @email_password_login_enabled + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=email, required=True, location="json") + parser.add_argument("language", type=str, required=False, location="json") + args = parser.parse_args() + + ip_address = extract_remote_ip(request) + if AccountService.is_email_send_ip_limit(ip_address): + raise EmailSendIpLimitError() + + if args["language"] is not None and args["language"] == "zh-Hans": + language = "zh-Hans" + else: + language = "en-US" + + with Session(db.engine) as session: + account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none() + token = None + if account is None: + raise AccountNotFound() + else: + token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language) + + return {"result": "success", "data": token} + + +class ForgotPasswordCheckApi(Resource): + @only_edition_enterprise + @setup_required + @email_password_login_enabled + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("email", type=str, required=True, location="json") + parser.add_argument("code", type=str, required=True, location="json") + parser.add_argument("token", type=str, required=True, nullable=False, location="json") + args = parser.parse_args() + + user_email = args["email"] + + is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(args["email"]) + if is_forgot_password_error_rate_limit: + raise EmailPasswordResetLimitError() + + token_data = AccountService.get_reset_password_data(args["token"]) + if token_data is None: + raise InvalidTokenError() + + if user_email != token_data.get("email"): + raise InvalidEmailError() + + if args["code"] != token_data.get("code"): + AccountService.add_forgot_password_error_rate_limit(args["email"]) + raise EmailCodeError() + + # Verified, revoke the first token + AccountService.revoke_reset_password_token(args["token"]) + + # Refresh token data by generating a new token + _, new_token = AccountService.generate_reset_password_token( + user_email, code=args["code"], additional_data={"phase": "reset"} + ) + + AccountService.reset_forgot_password_error_rate_limit(args["email"]) + return {"is_valid": True, "email": token_data.get("email"), "token": new_token} + + +class ForgotPasswordResetApi(Resource): + @only_edition_enterprise + @setup_required + @email_password_login_enabled + def post(self): + parser = reqparse.RequestParser() + parser.add_argument("token", type=str, required=True, nullable=False, location="json") + parser.add_argument("new_password", type=valid_password, required=True, nullable=False, location="json") + parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json") + args = parser.parse_args() + + # Validate passwords match + if args["new_password"] != args["password_confirm"]: + raise PasswordMismatchError() + + # Validate token and get reset data + reset_data = AccountService.get_reset_password_data(args["token"]) + if not reset_data: + raise InvalidTokenError() + # Must use token in reset phase + if reset_data.get("phase", "") != "reset": + raise InvalidTokenError() + + # Revoke token to prevent reuse + AccountService.revoke_reset_password_token(args["token"]) + + # Generate secure salt and hash password + salt = secrets.token_bytes(16) + password_hashed = hash_password(args["new_password"], salt) + + email = reset_data.get("email", "") + + with Session(db.engine) as session: + account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none() + + if account: + self._update_existing_account(account, password_hashed, salt, session) + else: + raise AccountNotFound() + + return {"result": "success"} + + def _update_existing_account(self, account, password_hashed, salt, session): + # Update existing account credentials + account.password = base64.b64encode(password_hashed).decode() + account.password_salt = base64.b64encode(salt).decode() + session.commit() + + +api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password") +api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity") +api.add_resource(ForgotPasswordResetApi, "/forgot-password/resets") diff --git a/api/controllers/web/login.py b/api/controllers/web/login.py index 06c2274440..01c4f4a262 100644 --- a/api/controllers/web/login.py +++ b/api/controllers/web/login.py @@ -1,12 +1,11 @@ -from flask import request from flask_restful import Resource, reqparse from jwt import InvalidTokenError # type: ignore -from werkzeug.exceptions import BadRequest import services from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError from controllers.console.error import AccountBannedError, AccountNotFound -from controllers.console.wraps import setup_required +from controllers.console.wraps import only_edition_enterprise, setup_required +from controllers.web import api from libs.helper import email from libs.password import valid_password from services.account_service import AccountService @@ -16,6 +15,8 @@ from services.webapp_auth_service import WebAppAuthService class LoginApi(Resource): """Resource for web app email/password login.""" + @setup_required + @only_edition_enterprise def post(self): """Authenticate user and login.""" parser = reqparse.RequestParser() @@ -23,10 +24,6 @@ class LoginApi(Resource): parser.add_argument("password", type=valid_password, required=True, location="json") args = parser.parse_args() - app_code = request.headers.get("X-App-Code") - if app_code is None: - raise BadRequest("X-App-Code header is missing.") - try: account = WebAppAuthService.authenticate(args["email"], args["password"]) except services.errors.account.AccountLoginError: @@ -36,12 +33,8 @@ class LoginApi(Resource): except services.errors.account.AccountNotFoundError: raise AccountNotFound() - WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) - - end_user = WebAppAuthService.create_end_user(email=args["email"], app_code=app_code) - - token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id) - return {"result": "success", "token": token} + token = WebAppAuthService.login(account=account) + return {"result": "success", "data": {"access_token": token}} # class LogoutApi(Resource): @@ -56,6 +49,7 @@ class LoginApi(Resource): class EmailCodeLoginSendEmailApi(Resource): @setup_required + @only_edition_enterprise def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=email, required=True, location="json") @@ -78,6 +72,7 @@ class EmailCodeLoginSendEmailApi(Resource): class EmailCodeLoginApi(Resource): @setup_required + @only_edition_enterprise def post(self): parser = reqparse.RequestParser() parser.add_argument("email", type=str, required=True, location="json") @@ -86,9 +81,6 @@ class EmailCodeLoginApi(Resource): args = parser.parse_args() user_email = args["email"] - app_code = request.headers.get("X-App-Code") - if app_code is None: - raise BadRequest("X-App-Code header is missing.") token_data = WebAppAuthService.get_email_code_login_data(args["token"]) if token_data is None: @@ -105,16 +97,12 @@ class EmailCodeLoginApi(Resource): if not account: raise AccountNotFound() - WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code) - - end_user = WebAppAuthService.create_end_user(email=user_email, app_code=app_code) - - token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id) + token = WebAppAuthService.login(account=account) AccountService.reset_login_error_rate_limit(args["email"]) - return {"result": "success", "token": token} + return {"result": "success", "data": {"access_token": token}} -# api.add_resource(LoginApi, "/login") +api.add_resource(LoginApi, "/login") # api.add_resource(LogoutApi, "/logout") -# api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") -# api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") +api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login") +api.add_resource(EmailCodeLoginApi, "/email-code-login/validity") diff --git a/api/controllers/web/passport.py b/api/controllers/web/passport.py index 154c6772b7..10c3cdcf0e 100644 --- a/api/controllers/web/passport.py +++ b/api/controllers/web/passport.py @@ -1,9 +1,11 @@ import uuid +from datetime import UTC, datetime, timedelta from flask import request from flask_restful import Resource from werkzeug.exceptions import NotFound, Unauthorized +from configs import dify_config from controllers.web import api from controllers.web.error import WebAppAuthRequiredError from extensions.ext_database import db @@ -11,6 +13,7 @@ from libs.passport import PassportService from models.model import App, EndUser, Site from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import FeatureService +from services.webapp_auth_service import WebAppAuthService, WebAppAuthType class PassportResource(Resource): @@ -20,10 +23,19 @@ class PassportResource(Resource): system_features = FeatureService.get_system_features() app_code = request.headers.get("X-App-Code") user_id = request.args.get("user_id") + web_app_access_token = request.args.get("web_app_access_token") if app_code is None: raise Unauthorized("X-App-Code header is missing.") + # exchange token for enterprise logined web user + enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token) + if enterprise_user_decoded: + # a web user has already logged in, exchange a token for this app without redirecting to the login page + return exchange_token_for_existing_web_user( + app_code=app_code, enterprise_user_decoded=enterprise_user_decoded + ) + if system_features.webapp_auth.enabled: app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) if not app_settings or not app_settings.access_mode == "public": @@ -84,6 +96,128 @@ class PassportResource(Resource): api.add_resource(PassportResource, "/passport") +def decode_enterprise_webapp_user_id(jwt_token: str | None): + """ + Decode the enterprise user session from the Authorization header. + """ + if not jwt_token: + return None + + decoded = PassportService().verify(jwt_token) + source = decoded.get("token_source") + if not source or source != "webapp_login_token": + raise Unauthorized("Invalid token source. Expected 'webapp_login_token'.") + return decoded + + +def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded: dict): + """ + Exchange a token for an existing web user session. + """ + user_id = enterprise_user_decoded.get("user_id") + end_user_id = enterprise_user_decoded.get("end_user_id") + session_id = enterprise_user_decoded.get("session_id") + user_auth_type = enterprise_user_decoded.get("auth_type") + if not user_auth_type: + raise Unauthorized("Missing auth_type in the token.") + + site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first() + if not site: + raise NotFound() + + app_model = db.session.query(App).filter(App.id == site.app_id).first() + if not app_model or app_model.status != "normal" or not app_model.enable_site: + raise NotFound() + + app_auth_type = WebAppAuthService.get_app_auth_type(app_code=app_code) + + if app_auth_type == WebAppAuthType.PUBLIC: + return _exchange_for_public_app_token(app_model, site, enterprise_user_decoded) + elif app_auth_type == WebAppAuthType.EXTERNAL and user_auth_type != "external": + raise WebAppAuthRequiredError("Please login as external user.") + elif app_auth_type == WebAppAuthType.INTERNAL and user_auth_type != "internal": + raise WebAppAuthRequiredError("Please login as internal user.") + + end_user = None + if end_user_id: + end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first() + if session_id: + end_user = ( + db.session.query(EndUser) + .filter( + EndUser.session_id == session_id, + EndUser.tenant_id == app_model.tenant_id, + EndUser.app_id == app_model.id, + ) + .first() + ) + if not end_user: + if not session_id: + raise NotFound("Missing session_id for existing web user.") + end_user = EndUser( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + type="browser", + is_anonymous=True, + session_id=session_id, + ) + db.session.add(end_user) + db.session.commit() + exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES) + exp = int(exp_dt.timestamp()) + payload = { + "iss": site.id, + "sub": "Web API Passport", + "app_id": site.app_id, + "app_code": site.code, + "user_id": user_id, + "end_user_id": end_user.id, + "auth_type": user_auth_type, + "granted_at": int(datetime.now(UTC).timestamp()), + "token_source": "webapp", + "exp": exp, + } + token: str = PassportService().issue(payload) + return { + "access_token": token, + } + + +def _exchange_for_public_app_token(app_model, site, token_decoded): + user_id = token_decoded.get("user_id") + end_user = None + if user_id: + end_user = ( + db.session.query(EndUser).filter(EndUser.app_id == app_model.id, EndUser.session_id == user_id).first() + ) + + if not end_user: + end_user = EndUser( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + type="browser", + is_anonymous=True, + session_id=generate_session_id(), + ) + + db.session.add(end_user) + db.session.commit() + + payload = { + "iss": site.app_id, + "sub": "Web API Passport", + "app_id": site.app_id, + "app_code": site.code, + "end_user_id": end_user.id, + } + + tk = PassportService().issue(payload) + + return { + "access_token": tk, + } + + def generate_session_id(): """ Generate a unique session ID. diff --git a/api/controllers/web/wraps.py b/api/controllers/web/wraps.py index 3bb029d6eb..154bddfc5c 100644 --- a/api/controllers/web/wraps.py +++ b/api/controllers/web/wraps.py @@ -1,3 +1,4 @@ +from datetime import UTC, datetime from functools import wraps from flask import request @@ -8,8 +9,9 @@ from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequire from extensions.ext_database import db from libs.passport import PassportService from models.model import App, EndUser, Site -from services.enterprise.enterprise_service import EnterpriseService +from services.enterprise.enterprise_service import EnterpriseService, WebAppSettings from services.feature_service import FeatureService +from services.webapp_auth_service import WebAppAuthService def validate_jwt_token(view=None): @@ -45,7 +47,8 @@ def decode_jwt_token(): raise Unauthorized("Invalid Authorization header format. Expected 'Bearer ' format.") decoded = PassportService().verify(tk) app_code = decoded.get("app_code") - app_model = db.session.query(App).filter(App.id == decoded["app_id"]).first() + app_id = decoded.get("app_id") + app_model = db.session.query(App).filter(App.id == app_id).first() site = db.session.query(Site).filter(Site.code == app_code).first() if not app_model: raise NotFound() @@ -53,23 +56,30 @@ def decode_jwt_token(): raise BadRequest("Site URL is no longer valid.") if app_model.enable_site is False: raise BadRequest("Site is disabled.") - end_user = db.session.query(EndUser).filter(EndUser.id == decoded["end_user_id"]).first() + end_user_id = decoded.get("end_user_id") + end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first() if not end_user: raise NotFound() # for enterprise webapp auth app_web_auth_enabled = False + webapp_settings = None if system_features.webapp_auth.enabled: - app_web_auth_enabled = ( - EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public" - ) + webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) + if not webapp_settings: + raise NotFound("Web app settings not found.") + app_web_auth_enabled = webapp_settings.access_mode != "public" _validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled) - _validate_user_accessibility(decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled) + _validate_user_accessibility( + decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled, webapp_settings + ) return app_model, end_user except Unauthorized as e: if system_features.webapp_auth.enabled: + if not app_code: + raise Unauthorized("Please re-login to access the web app.") app_web_auth_enabled = ( EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=str(app_code)).access_mode != "public" ) @@ -95,15 +105,41 @@ def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_au raise Unauthorized("webapp token expired.") -def _validate_user_accessibility(decoded, app_code, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool): +def _validate_user_accessibility( + decoded, + app_code, + app_web_auth_enabled: bool, + system_webapp_auth_enabled: bool, + webapp_settings: WebAppSettings | None, +): if system_webapp_auth_enabled and app_web_auth_enabled: # Check if the user is allowed to access the web app user_id = decoded.get("user_id") if not user_id: raise WebAppAuthRequiredError() - if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code): - raise WebAppAuthAccessDeniedError() + if not webapp_settings: + raise WebAppAuthRequiredError("Web app settings not found.") + + if WebAppAuthService.is_app_require_permission_check(access_mode=webapp_settings.access_mode): + if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code): + raise WebAppAuthAccessDeniedError() + + auth_type = decoded.get("auth_type") + granted_at = decoded.get("granted_at") + if not auth_type: + raise WebAppAuthAccessDeniedError("Missing auth_type in the token.") + if not granted_at: + raise WebAppAuthAccessDeniedError("Missing granted_at in the token.") + # check if sso has been updated + if auth_type == "external": + last_update_time = EnterpriseService.get_app_sso_settings_last_update_time() + if granted_at and datetime.fromtimestamp(granted_at, tz=UTC) < last_update_time: + raise WebAppAuthAccessDeniedError("SSO settings have been updated. Please re-login.") + elif auth_type == "internal": + last_update_time = EnterpriseService.get_workspace_sso_settings_last_update_time() + if granted_at and datetime.fromtimestamp(granted_at, tz=UTC) < last_update_time: + raise WebAppAuthAccessDeniedError("SSO settings have been updated. Please re-login.") class WebApiResource(Resource): diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 6998e4d29a..28bf4a9a23 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -3,6 +3,8 @@ import logging import uuid from typing import Optional, Union, cast +from sqlalchemy import select + from core.agent.entities import AgentEntity, AgentToolEntity from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfig @@ -161,10 +163,14 @@ class BaseAgentRunner(AppRunner): if parameter.type == ToolParameter.ToolParameterType.SELECT: enum = [option.value for option in parameter.options] if parameter.options else [] - message_tool.parameters["properties"][parameter.name] = { - "type": parameter_type, - "description": parameter.llm_description or "", - } + message_tool.parameters["properties"][parameter.name] = ( + { + "type": parameter_type, + "description": parameter.llm_description or "", + } + if parameter.input_schema is None + else parameter.input_schema + ) if len(enum) > 0: message_tool.parameters["properties"][parameter.name]["enum"] = enum @@ -254,10 +260,14 @@ class BaseAgentRunner(AppRunner): if parameter.type == ToolParameter.ToolParameterType.SELECT: enum = [option.value for option in parameter.options] if parameter.options else [] - prompt_tool.parameters["properties"][parameter.name] = { - "type": parameter_type, - "description": parameter.llm_description or "", - } + prompt_tool.parameters["properties"][parameter.name] = ( + { + "type": parameter_type, + "description": parameter.llm_description or "", + } + if parameter.input_schema is None + else parameter.input_schema + ) if len(enum) > 0: prompt_tool.parameters["properties"][parameter.name]["enum"] = enum @@ -409,12 +419,15 @@ class BaseAgentRunner(AppRunner): if isinstance(prompt_message, SystemPromptMessage): result.append(prompt_message) - messages: list[Message] = ( - db.session.query(Message) - .filter( - Message.conversation_id == self.message.conversation_id, + messages = ( + ( + db.session.execute( + select(Message) + .where(Message.conversation_id == self.message.conversation_id) + .order_by(Message.created_at.desc()) + ) ) - .order_by(Message.created_at.desc()) + .scalars() .all() ) diff --git a/api/core/agent/plugin_entities.py b/api/core/agent/plugin_entities.py index 691737b86a..a3438fc2c7 100644 --- a/api/core/agent/plugin_entities.py +++ b/api/core/agent/plugin_entities.py @@ -86,7 +86,7 @@ class AgentStrategyEntity(BaseModel): description: I18nObject = Field(..., description="The description of the agent strategy") output_schema: Optional[dict] = None features: Optional[list[AgentFeature]] = None - + meta_version: Optional[str] = None # pydantic configs model_config = ConfigDict(protected_namespaces=()) diff --git a/api/core/agent/strategy/plugin.py b/api/core/agent/strategy/plugin.py index 79b074cf95..4cfcfbf86a 100644 --- a/api/core/agent/strategy/plugin.py +++ b/api/core/agent/strategy/plugin.py @@ -15,10 +15,12 @@ class PluginAgentStrategy(BaseAgentStrategy): tenant_id: str declaration: AgentStrategyEntity + meta_version: str | None = None - def __init__(self, tenant_id: str, declaration: AgentStrategyEntity): + def __init__(self, tenant_id: str, declaration: AgentStrategyEntity, meta_version: str | None): self.tenant_id = tenant_id self.declaration = declaration + self.meta_version = meta_version def get_parameters(self) -> Sequence[AgentStrategyParameter]: return self.declaration.parameters diff --git a/api/core/app/app_config/easy_ui_based_app/dataset/manager.py b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py index 20189053f4..a5492d70bd 100644 --- a/api/core/app/app_config/easy_ui_based_app/dataset/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/dataset/manager.py @@ -138,14 +138,11 @@ class DatasetConfigManager: if not config.get("dataset_configs"): config["dataset_configs"] = {"retrieval_model": "single"} - if not config["dataset_configs"].get("datasets"): - config["dataset_configs"]["datasets"] = {"strategy": "router", "datasets": []} - if not isinstance(config["dataset_configs"], dict): raise ValueError("dataset_configs must be of object type") - if not isinstance(config["dataset_configs"], dict): - raise ValueError("dataset_configs must be of object type") + if not config["dataset_configs"].get("datasets"): + config["dataset_configs"]["datasets"] = {"strategy": "router", "datasets": []} need_manual_query_datasets = config.get("dataset_configs") and config["dataset_configs"].get( "datasets", {} diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index 3f31b1c3d5..75bd2f677a 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -104,6 +104,7 @@ class VariableEntity(BaseModel): Variable Entity. """ + # `variable` records the name of the variable in user inputs. variable: str label: str description: str = "" diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 8c85f91d7e..4b8f5ebe27 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -25,16 +25,23 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from core.prompt.utils.get_thread_messages_length import get_thread_messages_length -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository -from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository +from core.repositories import DifyCoreRepositoryFactory +from core.workflow.repositories.draft_variable_repository import ( + DraftVariableSaverFactory, +) from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom from services.conversation_service import ConversationService -from services.errors.message import MessageNotExistsError +from services.workflow_draft_variable_service import ( + DraftVarLoader, + WorkflowDraftVariableService, +) logger = logging.getLogger(__name__) @@ -115,6 +122,11 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): ) # parse files + # TODO(QuantumGhost): Move file parsing logic to the API controller layer + # for better separation of concerns. + # + # For implementation reference, see the `_parse_file` function and + # `DraftWorkflowNodeRunApi` class which handle this properly. files = args["files"] if args.get("files") else [] file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) if file_extra_config: @@ -170,14 +182,14 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING else: workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=workflow_triggered_from, ) # Create workflow node execution repository - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, @@ -247,19 +259,26 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): # Create session factory session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) # Create workflow execution(aka workflow run) repository - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, ) # Create workflow node execution repository - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP, ) + var_loader = DraftVarLoader( + engine=db.engine, + app_id=application_generate_entity.app_config.app_id, + tenant_id=application_generate_entity.app_config.tenant_id, + ) + draft_var_srv = WorkflowDraftVariableService(db.session()) + draft_var_srv.prefill_conversation_variable_default_values(workflow) return self._generate( workflow=workflow, @@ -270,6 +289,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_node_execution_repository=workflow_node_execution_repository, conversation=None, stream=streaming, + variable_loader=var_loader, ) def single_loop_generate( @@ -322,19 +342,26 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): # Create session factory session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) # Create workflow execution(aka workflow run) repository - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, ) # Create workflow node execution repository - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP, ) + var_loader = DraftVarLoader( + engine=db.engine, + app_id=application_generate_entity.app_config.app_id, + tenant_id=application_generate_entity.app_config.tenant_id, + ) + draft_var_srv = WorkflowDraftVariableService(db.session()) + draft_var_srv.prefill_conversation_variable_default_values(workflow) return self._generate( workflow=workflow, @@ -345,6 +372,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_node_execution_repository=workflow_node_execution_repository, conversation=None, stream=streaming, + variable_loader=var_loader, ) def _generate( @@ -358,6 +386,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_node_execution_repository: WorkflowNodeExecutionRepository, conversation: Optional[Conversation] = None, stream: bool = True, + variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER, ) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], Any, None]: """ Generate App response. @@ -366,6 +395,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param user: account or end user :param invoke_from: invoke from source :param application_generate_entity: application generate entity + :param workflow_execution_repository: repository for workflow execution :param workflow_node_execution_repository: repository for workflow node execution :param conversation: conversation :param stream: is stream @@ -399,20 +429,18 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - context=context, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + "context": context, + "variable_loader": variable_loader, + }, + ) worker_thread.start() @@ -427,6 +455,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, stream=stream, + draft_var_saver_factory=self._get_draft_var_saver_factory(invoke_from), ) return AdvancedChatAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from) @@ -439,6 +468,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation_id: str, message_id: str, context: contextvars.Context, + variable_loader: VariableLoader, ) -> None: """ Generate worker in a new thread. @@ -449,29 +479,12 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) - if message is None: - raise MessageNotExistsError("Message not exists") # chatbot app runner = AdvancedChatAppRunner( @@ -480,6 +493,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): conversation=conversation, message=message, dialogue_count=self._dialogue_count, + variable_loader=variable_loader, ) runner.run() @@ -513,6 +527,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): user: Union[Account, EndUser], workflow_execution_repository: WorkflowExecutionRepository, workflow_node_execution_repository: WorkflowNodeExecutionRepository, + draft_var_saver_factory: DraftVariableSaverFactory, stream: bool = False, ) -> Union[ChatbotAppBlockingResponse, Generator[ChatbotAppStreamResponse, None, None]]: """ @@ -539,6 +554,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator): workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, stream=stream, + draft_var_saver_factory=draft_var_saver_factory, ) try: diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index d9b3833862..af15324f46 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -16,9 +16,11 @@ from core.app.entities.queue_entities import ( QueueTextChunkEvent, ) from core.moderation.base import ModerationError +from core.variables.variables import VariableUnion from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey +from core.workflow.system_variable import SystemVariable +from core.workflow.variable_loader import VariableLoader from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from models.enums import UserFrom @@ -40,14 +42,17 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): conversation: Conversation, message: Message, dialogue_count: int, + variable_loader: VariableLoader, ) -> None: - super().__init__(queue_manager) - + super().__init__(queue_manager, variable_loader) self.application_generate_entity = application_generate_entity self.conversation = conversation self.message = message self._dialogue_count = dialogue_count + def _get_app_id(self) -> str: + return self.application_generate_entity.app_config.app_id + def run(self) -> None: app_config = self.application_generate_entity.app_config app_config = cast(AdvancedChatAppConfig, app_config) @@ -60,7 +65,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): if not workflow: raise ValueError("Workflow not initialized") - user_id = None + user_id: str | None = None if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}: end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first() if end_user: @@ -132,23 +137,25 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner): session.commit() # Create a variable pool. - system_inputs = { - SystemVariableKey.QUERY: query, - SystemVariableKey.FILES: files, - SystemVariableKey.CONVERSATION_ID: self.conversation.id, - SystemVariableKey.USER_ID: user_id, - SystemVariableKey.DIALOGUE_COUNT: self._dialogue_count, - SystemVariableKey.APP_ID: app_config.app_id, - SystemVariableKey.WORKFLOW_ID: app_config.workflow_id, - SystemVariableKey.WORKFLOW_EXECUTION_ID: self.application_generate_entity.workflow_run_id, - } + system_inputs = SystemVariable( + query=query, + files=files, + conversation_id=self.conversation.id, + user_id=user_id, + dialogue_count=self._dialogue_count, + app_id=app_config.app_id, + workflow_id=app_config.workflow_id, + workflow_execution_id=self.application_generate_entity.workflow_run_id, + ) # init variable pool variable_pool = VariablePool( system_variables=system_inputs, user_inputs=inputs, environment_variables=workflow.environment_variables, - conversation_variables=conversation_variables, + # Based on the definition of `VariableUnion`, + # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. + conversation_variables=cast(list[VariableUnion], conversation_variables), ) # init graph diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 8c5645bbb7..1dc9796d5b 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -61,11 +61,12 @@ from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk from core.model_runtime.entities.llm_entities import LLMUsage from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.entities.workflow_execution import WorkflowExecutionStatus, WorkflowType -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes import NodeType +from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.system_variable import SystemVariable from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager from events.message_event import message_was_created from extensions.ext_database import db @@ -94,6 +95,7 @@ class AdvancedChatAppGenerateTaskPipeline: dialogue_count: int, workflow_execution_repository: WorkflowExecutionRepository, workflow_node_execution_repository: WorkflowNodeExecutionRepository, + draft_var_saver_factory: DraftVariableSaverFactory, ) -> None: self._base_task_pipeline = BasedGenerateTaskPipeline( application_generate_entity=application_generate_entity, @@ -114,16 +116,16 @@ class AdvancedChatAppGenerateTaskPipeline: self._workflow_cycle_manager = WorkflowCycleManager( application_generate_entity=application_generate_entity, - workflow_system_variables={ - SystemVariableKey.QUERY: message.query, - SystemVariableKey.FILES: application_generate_entity.files, - SystemVariableKey.CONVERSATION_ID: conversation.id, - SystemVariableKey.USER_ID: user_session_id, - SystemVariableKey.DIALOGUE_COUNT: dialogue_count, - SystemVariableKey.APP_ID: application_generate_entity.app_config.app_id, - SystemVariableKey.WORKFLOW_ID: workflow.id, - SystemVariableKey.WORKFLOW_EXECUTION_ID: application_generate_entity.workflow_run_id, - }, + workflow_system_variables=SystemVariable( + query=message.query, + files=application_generate_entity.files, + conversation_id=conversation.id, + user_id=user_session_id, + dialogue_count=dialogue_count, + app_id=application_generate_entity.app_config.app_id, + workflow_id=workflow.id, + workflow_execution_id=application_generate_entity.workflow_run_id, + ), workflow_info=CycleManagerWorkflowInfo( workflow_id=workflow.id, workflow_type=WorkflowType(workflow.type), @@ -153,6 +155,7 @@ class AdvancedChatAppGenerateTaskPipeline: self._conversation_name_generate_thread: Thread | None = None self._recorded_files: list[Mapping[str, Any]] = [] self._workflow_run_id: str = "" + self._draft_var_saver_factory = draft_var_saver_factory def process(self) -> Union[ChatbotAppBlockingResponse, Generator[ChatbotAppStreamResponse, None, None]]: """ @@ -371,6 +374,7 @@ class AdvancedChatAppGenerateTaskPipeline: workflow_node_execution=workflow_node_execution, ) session.commit() + self._save_output_for_event(event, workflow_node_execution.id) if node_finish_resp: yield node_finish_resp @@ -390,6 +394,8 @@ class AdvancedChatAppGenerateTaskPipeline: task_id=self._application_generate_entity.task_id, workflow_node_execution=workflow_node_execution, ) + if isinstance(event, QueueNodeExceptionEvent): + self._save_output_for_event(event, workflow_node_execution.id) if node_finish_resp: yield node_finish_resp @@ -759,3 +765,15 @@ class AdvancedChatAppGenerateTaskPipeline: if not message: raise ValueError(f"Message not found: {self._message_id}") return message + + def _save_output_for_event(self, event: QueueNodeSucceededEvent | QueueNodeExceptionEvent, node_execution_id: str): + with Session(db.engine) as session, session.begin(): + saver = self._draft_var_saver_factory( + session=session, + app_id=self._application_generate_entity.app_config.app_id, + node_id=event.node_id, + node_type=event.node_type, + node_execution_id=node_execution_id, + enclosing_node_id=event.in_loop_id or event.in_iteration_id, + ) + saver.save(event.process_data, event.outputs) diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 158196f24d..edea6199d3 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping from typing import Any, Literal, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from configs import dify_config @@ -23,9 +23,9 @@ from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser from services.conversation_service import ConversationService -from services.errors.message import MessageNotExistsError logger = logging.getLogger(__name__) @@ -123,6 +123,11 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): override_model_config_dict["retriever_resource"] = {"enabled": True} # parse files + # TODO(QuantumGhost): Move file parsing logic to the API controller layer + # for better separation of concerns. + # + # For implementation reference, see the `_parse_file` function and + # `DraftWorkflowNodeRunApi` class which handle this properly. files = args.get("files") or [] file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) if file_extra_config: @@ -182,20 +187,17 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - context=context, - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - conversation_id=conversation.id, - message_id=message.id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "context": context, + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "conversation_id": conversation.id, + "message_id": message.id, + }, + ) worker_thread.start() @@ -229,29 +231,12 @@ class AgentChatAppGenerator(MessageBasedAppGenerator): :param message_id: message ID :return: """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) - if message is None: - raise MessageNotExistsError("Message not exists") # chatbot app runner = AgentChatAppRunner() diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index a83b75cc1a..beece1d77e 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -1,10 +1,20 @@ import json from collections.abc import Generator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional, Union, final + +from sqlalchemy.orm import Session from core.app.app_config.entities import VariableEntityType +from core.app.entities.app_invoke_entities import InvokeFrom from core.file import File, FileUploadConfig +from core.workflow.nodes.enums import NodeType +from core.workflow.repositories.draft_variable_repository import ( + DraftVariableSaver, + DraftVariableSaverFactory, + NoopDraftVariableSaver, +) from factories import file_factory +from services.workflow_draft_variable_service import DraftVariableSaver as DraftVariableSaverImpl if TYPE_CHECKING: from core.app.app_config.entities import VariableEntity @@ -159,3 +169,38 @@ class BaseAppGenerator: yield f"event: {message}\n\n" return gen() + + @final + @staticmethod + def _get_draft_var_saver_factory(invoke_from: InvokeFrom) -> DraftVariableSaverFactory: + if invoke_from == InvokeFrom.DEBUGGER: + + def draft_var_saver_factory( + session: Session, + app_id: str, + node_id: str, + node_type: NodeType, + node_execution_id: str, + enclosing_node_id: str | None = None, + ) -> DraftVariableSaver: + return DraftVariableSaverImpl( + session=session, + app_id=app_id, + node_id=node_id, + node_type=node_type, + node_execution_id=node_execution_id, + enclosing_node_id=enclosing_node_id, + ) + else: + + def draft_var_saver_factory( + session: Session, + app_id: str, + node_id: str, + node_type: NodeType, + node_execution_id: str, + enclosing_node_id: str | None = None, + ) -> DraftVariableSaver: + return NoopDraftVariableSaver() + + return draft_var_saver_factory diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index c813dbb9d1..a3f0cf7f9f 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -1,3 +1,4 @@ +import logging import time from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, Optional, Union @@ -33,6 +34,8 @@ from models.model import App, AppMode, Message, MessageAnnotation if TYPE_CHECKING: from core.file.models import File +_logger = logging.getLogger(__name__) + class AppRunner: def get_pre_calculate_rest_tokens( @@ -298,7 +301,7 @@ class AppRunner: ) def _handle_invoke_result_stream( - self, invoke_result: Generator, queue_manager: AppQueueManager, agent: bool + self, invoke_result: Generator[LLMResultChunk, None, None], queue_manager: AppQueueManager, agent: bool ) -> None: """ Handle invoke result @@ -317,18 +320,28 @@ class AppRunner: else: queue_manager.publish(QueueAgentMessageEvent(chunk=result), PublishFrom.APPLICATION_MANAGER) - text += result.delta.message.content + message = result.delta.message + if isinstance(message.content, str): + text += message.content + elif isinstance(message.content, list): + for content in message.content: + if not isinstance(content, str): + # TODO(QuantumGhost): Add multimodal output support for easy ui. + _logger.warning("received multimodal output, type=%s", type(content)) + text += content.data + else: + text += content # failback to str if not model: model = result.model if not prompt_messages: - prompt_messages = result.prompt_messages + prompt_messages = list(result.prompt_messages) if result.delta.usage: usage = result.delta.usage - if not usage: + if usage is None: usage = LLMUsage.empty_usage() llm_result = LLMResult( diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index a1329cb938..a28c106ce9 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -25,7 +25,6 @@ from factories import file_factory from models.account import Account from models.model import App, EndUser from services.conversation_service import ConversationService -from services.errors.message import MessageNotExistsError logger = logging.getLogger(__name__) @@ -115,6 +114,11 @@ class ChatAppGenerator(MessageBasedAppGenerator): override_model_config_dict["retriever_resource"] = {"enabled": True} # parse files + # TODO(QuantumGhost): Move file parsing logic to the API controller layer + # for better separation of concerns. + # + # For implementation reference, see the `_parse_file` function and + # `DraftWorkflowNodeRunApi` class which handle this properly. files = args["files"] if args.get("files") else [] file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) if file_extra_config: @@ -219,8 +223,6 @@ class ChatAppGenerator(MessageBasedAppGenerator): # get conversation and message conversation = self._get_conversation(conversation_id) message = self._get_message(message_id) - if message is None: - raise MessageNotExistsError("Message not exists") # chatbot app runner = ChatAppRunner() diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index 6f524a5872..34a1da2227 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -44,10 +44,12 @@ from core.app.entities.task_entities import ( ) from core.file import FILE_MODEL_IDENTITY, File from core.tools.tool_manager import ToolManager +from core.variables.segments import ArrayFileSegment, FileSegment, Segment from core.workflow.entities.workflow_execution import WorkflowExecution from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution, WorkflowNodeExecutionStatus from core.workflow.nodes import NodeType from core.workflow.nodes.tool.entities import ToolNodeData +from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter from models import ( Account, CreatorUserRole, @@ -125,7 +127,7 @@ class WorkflowResponseConverter: id=workflow_execution.id_, workflow_id=workflow_execution.workflow_id, status=workflow_execution.status, - outputs=workflow_execution.outputs, + outputs=WorkflowRuntimeTypeConverter().to_json_encodable(workflow_execution.outputs), error=workflow_execution.error_message, elapsed_time=workflow_execution.elapsed_time, total_tokens=workflow_execution.total_tokens, @@ -202,6 +204,8 @@ class WorkflowResponseConverter: if not workflow_node_execution.finished_at: return None + json_converter = WorkflowRuntimeTypeConverter() + return NodeFinishStreamResponse( task_id=task_id, workflow_run_id=workflow_node_execution.workflow_execution_id, @@ -214,7 +218,7 @@ class WorkflowResponseConverter: predecessor_node_id=workflow_node_execution.predecessor_node_id, inputs=workflow_node_execution.inputs, process_data=workflow_node_execution.process_data, - outputs=workflow_node_execution.outputs, + outputs=json_converter.to_json_encodable(workflow_node_execution.outputs), status=workflow_node_execution.status, error=workflow_node_execution.error, elapsed_time=workflow_node_execution.elapsed_time, @@ -245,6 +249,8 @@ class WorkflowResponseConverter: if not workflow_node_execution.finished_at: return None + json_converter = WorkflowRuntimeTypeConverter() + return NodeRetryStreamResponse( task_id=task_id, workflow_run_id=workflow_node_execution.workflow_execution_id, @@ -257,7 +263,7 @@ class WorkflowResponseConverter: predecessor_node_id=workflow_node_execution.predecessor_node_id, inputs=workflow_node_execution.inputs, process_data=workflow_node_execution.process_data, - outputs=workflow_node_execution.outputs, + outputs=json_converter.to_json_encodable(workflow_node_execution.outputs), status=workflow_node_execution.status, error=workflow_node_execution.error, elapsed_time=workflow_node_execution.elapsed_time, @@ -376,6 +382,7 @@ class WorkflowResponseConverter: workflow_execution_id: str, event: QueueIterationCompletedEvent, ) -> IterationNodeCompletedStreamResponse: + json_converter = WorkflowRuntimeTypeConverter() return IterationNodeCompletedStreamResponse( task_id=task_id, workflow_run_id=workflow_execution_id, @@ -384,7 +391,7 @@ class WorkflowResponseConverter: node_id=event.node_id, node_type=event.node_type.value, title=event.node_data.title, - outputs=event.outputs, + outputs=json_converter.to_json_encodable(event.outputs), created_at=int(time.time()), extras={}, inputs=event.inputs or {}, @@ -463,7 +470,7 @@ class WorkflowResponseConverter: node_id=event.node_id, node_type=event.node_type.value, title=event.node_data.title, - outputs=event.outputs, + outputs=WorkflowRuntimeTypeConverter().to_json_encodable(event.outputs), created_at=int(time.time()), extras={}, inputs=event.inputs or {}, @@ -500,7 +507,8 @@ class WorkflowResponseConverter: # Convert to tuple to match Sequence type return tuple(flattened_files) - def _fetch_files_from_variable_value(self, value: Union[dict, list]) -> Sequence[Mapping[str, Any]]: + @classmethod + def _fetch_files_from_variable_value(cls, value: Union[dict, list, Segment]) -> Sequence[Mapping[str, Any]]: """ Fetch files from variable value :param value: variable value @@ -509,20 +517,30 @@ class WorkflowResponseConverter: if not value: return [] - files = [] - if isinstance(value, list): + files: list[Mapping[str, Any]] = [] + if isinstance(value, FileSegment): + files.append(value.value.to_dict()) + elif isinstance(value, ArrayFileSegment): + files.extend([i.to_dict() for i in value.value]) + elif isinstance(value, File): + files.append(value.to_dict()) + elif isinstance(value, list): for item in value: - file = self._get_file_var_from_value(item) + file = cls._get_file_var_from_value(item) if file: files.append(file) - elif isinstance(value, dict): - file = self._get_file_var_from_value(value) + elif isinstance( + value, + dict, + ): + file = cls._get_file_var_from_value(value) if file: files.append(file) return files - def _get_file_var_from_value(self, value: Union[dict, list]) -> Mapping[str, Any] | None: + @classmethod + def _get_file_var_from_value(cls, value: Union[dict, list]) -> Mapping[str, Any] | None: """ Get file var from value :param value: variable value diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index adcbaad3ec..966a6f1d66 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -101,6 +101,11 @@ class CompletionAppGenerator(MessageBasedAppGenerator): ) # parse files + # TODO(QuantumGhost): Move file parsing logic to the API controller layer + # for better separation of concerns. + # + # For implementation reference, see the `_parse_file` function and + # `DraftWorkflowNodeRunApi` class which handle this properly. files = args["files"] if args.get("files") else [] file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) if file_extra_config: @@ -196,8 +201,6 @@ class CompletionAppGenerator(MessageBasedAppGenerator): try: # get message message = self._get_message(message_id) - if message is None: - raise MessageNotExistsError() # chatbot app runner = CompletionAppRunner() diff --git a/api/core/app/apps/message_based_app_generator.py b/api/core/app/apps/message_based_app_generator.py index 58b94f4d43..e84d59209d 100644 --- a/api/core/app/apps/message_based_app_generator.py +++ b/api/core/app/apps/message_based_app_generator.py @@ -29,6 +29,7 @@ from models.enums import CreatorUserRole from models.model import App, AppMode, AppModelConfig, Conversation, EndUser, Message, MessageFile from services.errors.app_model_config import AppModelConfigBrokenError from services.errors.conversation import ConversationNotExistsError +from services.errors.message import MessageNotExistsError logger = logging.getLogger(__name__) @@ -251,7 +252,7 @@ class MessageBasedAppGenerator(BaseAppGenerator): return introduction or "" - def _get_conversation(self, conversation_id: str): + def _get_conversation(self, conversation_id: str) -> Conversation: """ Get conversation by conversation id :param conversation_id: conversation id @@ -260,11 +261,11 @@ class MessageBasedAppGenerator(BaseAppGenerator): conversation = db.session.query(Conversation).filter(Conversation.id == conversation_id).first() if not conversation: - raise ConversationNotExistsError() + raise ConversationNotExistsError("Conversation not exists") return conversation - def _get_message(self, message_id: str) -> Optional[Message]: + def _get_message(self, message_id: str) -> Message: """ Get message by message id :param message_id: message id @@ -272,4 +273,7 @@ class MessageBasedAppGenerator(BaseAppGenerator): """ message = db.session.query(Message).filter(Message.id == message_id).first() + if message is None: + raise MessageNotExistsError("Message not exists") + return message diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index f4aec3479b..2f9632e97d 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Generator, Mapping, Sequence from typing import Any, Literal, Optional, Union, overload -from flask import Flask, copy_current_request_context, current_app, has_request_context +from flask import Flask, current_app from pydantic import ValidationError from sqlalchemy.orm import sessionmaker @@ -23,14 +23,17 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse from core.model_runtime.errors.invoke import InvokeAuthorizationError from core.ops.ops_trace_manager import TraceQueueManager -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository -from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository +from core.repositories import DifyCoreRepositoryFactory +from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from extensions.ext_database import db from factories import file_factory +from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom +from services.workflow_draft_variable_service import DraftVarLoader, WorkflowDraftVariableService logger = logging.getLogger(__name__) @@ -93,6 +96,11 @@ class WorkflowAppGenerator(BaseAppGenerator): files: Sequence[Mapping[str, Any]] = args.get("files") or [] # parse files + # TODO(QuantumGhost): Move file parsing logic to the API controller layer + # for better separation of concerns. + # + # For implementation reference, see the `_parse_file` function and + # `DraftWorkflowNodeRunApi` class which handle this properly. file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) system_files = file_factory.build_from_mappings( mappings=files, @@ -147,14 +155,14 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING else: workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=workflow_triggered_from, ) # Create workflow node execution repository - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, @@ -185,6 +193,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow_node_execution_repository: WorkflowNodeExecutionRepository, streaming: bool = True, workflow_thread_pool_id: Optional[str] = None, + variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER, ) -> Union[Mapping[str, Any], Generator[str | Mapping[str, Any], None, None]]: """ Generate App response. @@ -194,6 +203,7 @@ class WorkflowAppGenerator(BaseAppGenerator): :param user: account or end user :param application_generate_entity: application generate entity :param invoke_from: invoke from source + :param workflow_execution_repository: repository for workflow execution :param workflow_node_execution_repository: repository for workflow node execution :param streaming: is stream :param workflow_thread_pool_id: workflow thread pool id @@ -209,22 +219,27 @@ class WorkflowAppGenerator(BaseAppGenerator): # new thread with request context and contextvars context = contextvars.copy_context() - @copy_current_request_context - def worker_with_context(): - # Run the worker within the copied context - return context.run( - self._generate_worker, - flask_app=current_app._get_current_object(), # type: ignore - application_generate_entity=application_generate_entity, - queue_manager=queue_manager, - context=context, - workflow_thread_pool_id=workflow_thread_pool_id, - ) - - worker_thread = threading.Thread(target=worker_with_context) + # release database connection, because the following new thread operations may take a long time + db.session.close() + + worker_thread = threading.Thread( + target=self._generate_worker, + kwargs={ + "flask_app": current_app._get_current_object(), # type: ignore + "application_generate_entity": application_generate_entity, + "queue_manager": queue_manager, + "context": context, + "workflow_thread_pool_id": workflow_thread_pool_id, + "variable_loader": variable_loader, + }, + ) worker_thread.start() + draft_var_saver_factory = self._get_draft_var_saver_factory( + invoke_from, + ) + # return response or stream generator response = self._handle_response( application_generate_entity=application_generate_entity, @@ -233,6 +248,7 @@ class WorkflowAppGenerator(BaseAppGenerator): user=user, workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, + draft_var_saver_factory=draft_var_saver_factory, stream=streaming, ) @@ -289,21 +305,26 @@ class WorkflowAppGenerator(BaseAppGenerator): # Create session factory session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) # Create workflow execution(aka workflow run) repository - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, ) # Create workflow node execution repository - session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) - - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP, ) + draft_var_srv = WorkflowDraftVariableService(db.session()) + draft_var_srv.prefill_conversation_variable_default_values(workflow) + var_loader = DraftVarLoader( + engine=db.engine, + app_id=application_generate_entity.app_config.app_id, + tenant_id=application_generate_entity.app_config.tenant_id, + ) return self._generate( app_model=app_model, @@ -314,6 +335,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, streaming=streaming, + variable_loader=var_loader, ) def single_loop_generate( @@ -365,22 +387,26 @@ class WorkflowAppGenerator(BaseAppGenerator): # Create session factory session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) # Create workflow execution(aka workflow run) repository - workflow_execution_repository = SQLAlchemyWorkflowExecutionRepository( + workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowRunTriggeredFrom.DEBUGGING, ) # Create workflow node execution repository - session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) - - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=user, app_id=application_generate_entity.app_config.app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP, ) - + draft_var_srv = WorkflowDraftVariableService(db.session()) + draft_var_srv.prefill_conversation_variable_default_values(workflow) + var_loader = DraftVarLoader( + engine=db.engine, + app_id=application_generate_entity.app_config.app_id, + tenant_id=application_generate_entity.app_config.tenant_id, + ) return self._generate( app_model=app_model, workflow=workflow, @@ -390,6 +416,7 @@ class WorkflowAppGenerator(BaseAppGenerator): workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, streaming=streaming, + variable_loader=var_loader, ) def _generate_worker( @@ -398,6 +425,7 @@ class WorkflowAppGenerator(BaseAppGenerator): application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager, context: contextvars.Context, + variable_loader: VariableLoader, workflow_thread_pool_id: Optional[str] = None, ) -> None: """ @@ -408,29 +436,15 @@ class WorkflowAppGenerator(BaseAppGenerator): :param workflow_thread_pool_id: workflow thread pool id :return: """ - for var, val in context.items(): - var.set(val) - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - # workflow app runner = WorkflowAppRunner( application_generate_entity=application_generate_entity, queue_manager=queue_manager, workflow_thread_pool_id=workflow_thread_pool_id, + variable_loader=variable_loader, ) runner.run() @@ -461,6 +475,7 @@ class WorkflowAppGenerator(BaseAppGenerator): user: Union[Account, EndUser], workflow_execution_repository: WorkflowExecutionRepository, workflow_node_execution_repository: WorkflowNodeExecutionRepository, + draft_var_saver_factory: DraftVariableSaverFactory, stream: bool = False, ) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]: """ @@ -481,6 +496,7 @@ class WorkflowAppGenerator(BaseAppGenerator): user=user, workflow_execution_repository=workflow_execution_repository, workflow_node_execution_repository=workflow_node_execution_repository, + draft_var_saver_factory=draft_var_saver_factory, stream=stream, ) diff --git a/api/core/app/apps/workflow/app_runner.py b/api/core/app/apps/workflow/app_runner.py index b59e34e222..3a66ffa578 100644 --- a/api/core/app/apps/workflow/app_runner.py +++ b/api/core/app/apps/workflow/app_runner.py @@ -11,7 +11,8 @@ from core.app.entities.app_invoke_entities import ( ) from core.workflow.callbacks import WorkflowCallback, WorkflowLoggingCallback from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey +from core.workflow.system_variable import SystemVariable +from core.workflow.variable_loader import VariableLoader from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from models.enums import UserFrom @@ -30,6 +31,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): self, application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager, + variable_loader: VariableLoader, workflow_thread_pool_id: Optional[str] = None, ) -> None: """ @@ -37,10 +39,13 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): :param queue_manager: application queue manager :param workflow_thread_pool_id: workflow thread pool id """ + super().__init__(queue_manager, variable_loader) self.application_generate_entity = application_generate_entity - self.queue_manager = queue_manager self.workflow_thread_pool_id = workflow_thread_pool_id + def _get_app_id(self) -> str: + return self.application_generate_entity.app_config.app_id + def run(self) -> None: """ Run application @@ -90,13 +95,14 @@ class WorkflowAppRunner(WorkflowBasedAppRunner): files = self.application_generate_entity.files # Create a variable pool. - system_inputs = { - SystemVariableKey.FILES: files, - SystemVariableKey.USER_ID: user_id, - SystemVariableKey.APP_ID: app_config.app_id, - SystemVariableKey.WORKFLOW_ID: app_config.workflow_id, - SystemVariableKey.WORKFLOW_EXECUTION_ID: self.application_generate_entity.workflow_execution_id, - } + + system_inputs = SystemVariable( + files=files, + user_id=user_id, + app_id=app_config.app_id, + workflow_id=app_config.workflow_id, + workflow_execution_id=self.application_generate_entity.workflow_execution_id, + ) variable_pool = VariablePool( system_variables=system_inputs, diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 1734dbb598..7adc03e9c3 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -3,7 +3,6 @@ import time from collections.abc import Generator from typing import Optional, Union -from sqlalchemy import select from sqlalchemy.orm import Session from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME @@ -55,9 +54,10 @@ from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTas from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.entities.workflow_execution import WorkflowExecution, WorkflowExecutionStatus, WorkflowType -from core.workflow.enums import SystemVariableKey +from core.workflow.repositories.draft_variable_repository import DraftVariableSaverFactory from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.system_variable import SystemVariable from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager from extensions.ext_database import db from models.account import Account @@ -67,7 +67,6 @@ from models.workflow import ( Workflow, WorkflowAppLog, WorkflowAppLogCreatedFrom, - WorkflowRun, ) logger = logging.getLogger(__name__) @@ -87,6 +86,7 @@ class WorkflowAppGenerateTaskPipeline: stream: bool, workflow_execution_repository: WorkflowExecutionRepository, workflow_node_execution_repository: WorkflowNodeExecutionRepository, + draft_var_saver_factory: DraftVariableSaverFactory, ) -> None: self._base_task_pipeline = BasedGenerateTaskPipeline( application_generate_entity=application_generate_entity, @@ -107,13 +107,13 @@ class WorkflowAppGenerateTaskPipeline: self._workflow_cycle_manager = WorkflowCycleManager( application_generate_entity=application_generate_entity, - workflow_system_variables={ - SystemVariableKey.FILES: application_generate_entity.files, - SystemVariableKey.USER_ID: user_session_id, - SystemVariableKey.APP_ID: application_generate_entity.app_config.app_id, - SystemVariableKey.WORKFLOW_ID: workflow.id, - SystemVariableKey.WORKFLOW_EXECUTION_ID: application_generate_entity.workflow_execution_id, - }, + workflow_system_variables=SystemVariable( + files=application_generate_entity.files, + user_id=user_session_id, + app_id=application_generate_entity.app_config.app_id, + workflow_id=workflow.id, + workflow_execution_id=application_generate_entity.workflow_execution_id, + ), workflow_info=CycleManagerWorkflowInfo( workflow_id=workflow.id, workflow_type=WorkflowType(workflow.type), @@ -131,6 +131,8 @@ class WorkflowAppGenerateTaskPipeline: self._application_generate_entity = application_generate_entity self._workflow_features_dict = workflow.features_dict self._workflow_run_id = "" + self._invoke_from = queue_manager._invoke_from + self._draft_var_saver_factory = draft_var_saver_factory def process(self) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]: """ @@ -322,6 +324,8 @@ class WorkflowAppGenerateTaskPipeline: workflow_node_execution=workflow_node_execution, ) + self._save_output_for_event(event, workflow_node_execution.id) + if node_success_response: yield node_success_response elif isinstance( @@ -339,6 +343,8 @@ class WorkflowAppGenerateTaskPipeline: task_id=self._application_generate_entity.task_id, workflow_node_execution=workflow_node_execution, ) + if isinstance(event, QueueNodeExceptionEvent): + self._save_output_for_event(event, workflow_node_execution.id) if node_failed_response: yield node_failed_response @@ -554,8 +560,6 @@ class WorkflowAppGenerateTaskPipeline: tts_publisher.publish(None) def _save_workflow_app_log(self, *, session: Session, workflow_execution: WorkflowExecution) -> None: - workflow_run = session.scalar(select(WorkflowRun).where(WorkflowRun.id == workflow_execution.id_)) - assert workflow_run is not None invoke_from = self._application_generate_entity.invoke_from if invoke_from == InvokeFrom.SERVICE_API: created_from = WorkflowAppLogCreatedFrom.SERVICE_API @@ -568,10 +572,10 @@ class WorkflowAppGenerateTaskPipeline: return workflow_app_log = WorkflowAppLog() - workflow_app_log.tenant_id = workflow_run.tenant_id - workflow_app_log.app_id = workflow_run.app_id - workflow_app_log.workflow_id = workflow_run.workflow_id - workflow_app_log.workflow_run_id = workflow_run.id + workflow_app_log.tenant_id = self._application_generate_entity.app_config.tenant_id + workflow_app_log.app_id = self._application_generate_entity.app_config.app_id + workflow_app_log.workflow_id = workflow_execution.workflow_id + workflow_app_log.workflow_run_id = workflow_execution.id_ workflow_app_log.created_from = created_from.value workflow_app_log.created_by_role = self._created_by_role workflow_app_log.created_by = self._user_id @@ -593,3 +597,15 @@ class WorkflowAppGenerateTaskPipeline: ) return response + + def _save_output_for_event(self, event: QueueNodeSucceededEvent | QueueNodeExceptionEvent, node_execution_id: str): + with Session(db.engine) as session, session.begin(): + saver = self._draft_var_saver_factory( + session=session, + app_id=self._application_generate_entity.app_config.app_id, + node_id=event.node_id, + node_type=event.node_type, + node_execution_id=node_execution_id, + enclosing_node_id=event.in_loop_id or event.in_iteration_id, + ) + saver.save(event.process_data, event.outputs) diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index facc24b4ca..2f4d234ecd 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -62,6 +62,8 @@ from core.workflow.graph_engine.entities.event import ( from core.workflow.graph_engine.entities.graph import Graph from core.workflow.nodes import NodeType from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING +from core.workflow.system_variable import SystemVariable +from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from models.model import App @@ -69,8 +71,12 @@ from models.workflow import Workflow class WorkflowBasedAppRunner(AppRunner): - def __init__(self, queue_manager: AppQueueManager): + def __init__(self, queue_manager: AppQueueManager, variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER) -> None: self.queue_manager = queue_manager + self._variable_loader = variable_loader + + def _get_app_id(self) -> str: + raise NotImplementedError("not implemented") def _init_graph(self, graph_config: Mapping[str, Any]) -> Graph: """ @@ -161,7 +167,7 @@ class WorkflowBasedAppRunner(AppRunner): # init variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, environment_variables=workflow.environment_variables, ) @@ -173,6 +179,13 @@ class WorkflowBasedAppRunner(AppRunner): except NotImplementedError: variable_mapping = {} + load_into_variable_pool( + variable_loader=self._variable_loader, + variable_pool=variable_pool, + variable_mapping=variable_mapping, + user_inputs=user_inputs, + ) + WorkflowEntry.mapping_user_inputs_to_variable_pool( variable_mapping=variable_mapping, user_inputs=user_inputs, @@ -251,7 +264,7 @@ class WorkflowBasedAppRunner(AppRunner): # init variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, environment_variables=workflow.environment_variables, ) @@ -262,6 +275,12 @@ class WorkflowBasedAppRunner(AppRunner): ) except NotImplementedError: variable_mapping = {} + load_into_variable_pool( + self._variable_loader, + variable_pool=variable_pool, + variable_mapping=variable_mapping, + user_inputs=user_inputs, + ) WorkflowEntry.mapping_user_inputs_to_variable_pool( variable_mapping=variable_mapping, @@ -376,6 +395,7 @@ class WorkflowBasedAppRunner(AppRunner): in_loop_id=event.in_loop_id, ) ) + elif isinstance(event, NodeRunFailedEvent): self._publish_event( QueueNodeFailedEvent( @@ -438,6 +458,7 @@ class WorkflowBasedAppRunner(AppRunner): in_loop_id=event.in_loop_id, ) ) + elif isinstance(event, NodeInIterationFailedEvent): self._publish_event( QueueNodeInIterationFailedEvent( diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index c0d99693b0..65ed267959 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -17,9 +17,24 @@ class InvokeFrom(Enum): Invoke From. """ + # SERVICE_API indicates that this invocation is from an API call to Dify app. + # + # Description of service api in Dify docs: + # https://docs.dify.ai/en/guides/application-publishing/developing-with-apis SERVICE_API = "service-api" + + # WEB_APP indicates that this invocation is from + # the web app of the workflow (or chatflow). + # + # Description of web app in Dify docs: + # https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README WEB_APP = "web-app" + + # EXPLORE indicates that this invocation is from + # the workflow (or chatflow) explore page. EXPLORE = "explore" + # DEBUGGER indicates that this invocation is from + # the workflow (or chatflow) edit page. DEBUGGER = "debugger" @classmethod diff --git a/api/core/app/task_pipeline/based_generate_task_pipeline.py b/api/core/app/task_pipeline/based_generate_task_pipeline.py index 5331c0cc94..3ed0c3352f 100644 --- a/api/core/app/task_pipeline/based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/based_generate_task_pipeline.py @@ -19,6 +19,7 @@ from core.app.entities.task_entities import ( from core.errors.error import QuotaExceededError from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from core.moderation.output_moderation import ModerationRule, OutputModeration +from models.enums import MessageStatus from models.model import Message logger = logging.getLogger(__name__) @@ -62,7 +63,7 @@ class BasedGenerateTaskPipeline: return err err_desc = self._error_to_desc(err) - message.status = "error" + message.status = MessageStatus.ERROR message.error = err_desc return err diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index 1ea50a5778..3c8c7bb5a2 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -48,6 +48,7 @@ from core.model_manager import ModelInstance from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, + TextPromptMessageContent, ) from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.ops.entities.trace_entity import TraceTaskName @@ -309,6 +310,23 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline): delta_text = chunk.delta.message.content if delta_text is None: continue + if isinstance(chunk.delta.message.content, list): + delta_text = "" + for content in chunk.delta.message.content: + logger.debug( + "The content type %s in LLM chunk delta message content.: %r", type(content), content + ) + if isinstance(content, TextPromptMessageContent): + delta_text += content.data + elif isinstance(content, str): + delta_text += content # failback to str + else: + logger.warning( + "Unsupported content type %s in LLM chunk delta message content.: %r", + type(content), + content, + ) + continue if not self._task_state.llm_result.prompt_messages: self._task_state.llm_result.prompt_messages = chunk.prompt_messages @@ -377,6 +395,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline): message.provider_response_latency = time.perf_counter() - self._start_at message.total_price = usage.total_price message.currency = usage.currency + self._task_state.llm_result.usage.latency = message.provider_response_latency message.message_metadata = self._task_state.metadata.model_dump_json() if trace_manager: diff --git a/api/core/entities/parameter_entities.py b/api/core/entities/parameter_entities.py index e5282b1cbe..fbd62437e6 100644 --- a/api/core/entities/parameter_entities.py +++ b/api/core/entities/parameter_entities.py @@ -16,7 +16,15 @@ class CommonParameterType(StrEnum): TOOLS_SELECTOR = "array[tools]" ANY = "any" + # Dynamic select parameter + # Once you are not sure about the available options until authorization is done + # eg: Select a Slack channel from a Slack workspace + DYNAMIC_SELECT = "dynamic-select" + # TOOL_SELECTOR = "tool-selector" + # MCP object and array type parameters + ARRAY = "array" + OBJECT = "object" class AppSelectorScope(StrEnum): diff --git a/api/core/file/constants.py b/api/core/file/constants.py index ce1d238e93..0665ed7e0d 100644 --- a/api/core/file/constants.py +++ b/api/core/file/constants.py @@ -1 +1,11 @@ +from typing import Any + +# TODO(QuantumGhost): Refactor variable type identification. Instead of directly +# comparing `dify_model_identity` with constants throughout the codebase, extract +# this logic into a dedicated function. This would encapsulate the implementation +# details of how different variable types are identified. FILE_MODEL_IDENTITY = "__dify__file__" + + +def maybe_file_object(o: Any) -> bool: + return isinstance(o, dict) and o.get("dify_model_identity") == FILE_MODEL_IDENTITY diff --git a/api/core/file/helpers.py b/api/core/file/helpers.py index 73fabdb11b..335ad2266a 100644 --- a/api/core/file/helpers.py +++ b/api/core/file/helpers.py @@ -21,7 +21,9 @@ def get_signed_file_url(upload_file_id: str) -> str: def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str: - url = f"{dify_config.FILES_URL}/files/upload/for-plugin" + # Plugin access should use internal URL for Docker network communication + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL + url = f"{base_url}/files/upload/for-plugin" if user_id is None: user_id = "DEFAULT-USER" diff --git a/api/core/file/models.py b/api/core/file/models.py index aa3b5f629c..f61334e7bc 100644 --- a/api/core/file/models.py +++ b/api/core/file/models.py @@ -51,7 +51,7 @@ class File(BaseModel): # It should be set to `ToolFile.id` when `transfer_method` is `tool_file`. related_id: Optional[str] = None filename: Optional[str] = None - extension: Optional[str] = Field(default=None, description="File extension, should contains dot") + extension: Optional[str] = Field(default=None, description="File extension, should contain dot") mime_type: Optional[str] = None size: int = -1 diff --git a/api/core/file/upload_file_parser.py b/api/core/file/upload_file_parser.py deleted file mode 100644 index 96b2884811..0000000000 --- a/api/core/file/upload_file_parser.py +++ /dev/null @@ -1,67 +0,0 @@ -import base64 -import logging -import time -from typing import Optional - -from configs import dify_config -from constants import IMAGE_EXTENSIONS -from core.helper.url_signer import UrlSigner -from extensions.ext_storage import storage - - -class UploadFileParser: - @classmethod - def get_image_data(cls, upload_file, force_url: bool = False) -> Optional[str]: - if not upload_file: - return None - - if upload_file.extension not in IMAGE_EXTENSIONS: - return None - - if dify_config.MULTIMODAL_SEND_FORMAT == "url" or force_url: - return cls.get_signed_temp_image_url(upload_file.id) - else: - # get image file base64 - try: - data = storage.load(upload_file.key) - except FileNotFoundError: - logging.exception(f"File not found: {upload_file.key}") - return None - - encoded_string = base64.b64encode(data).decode("utf-8") - return f"data:{upload_file.mime_type};base64,{encoded_string}" - - @classmethod - def get_signed_temp_image_url(cls, upload_file_id) -> str: - """ - get signed url from upload file - - :param upload_file_id: the id of UploadFile object - :return: - """ - base_url = dify_config.FILES_URL - image_preview_url = f"{base_url}/files/{upload_file_id}/image-preview" - - return UrlSigner.get_signed_url(url=image_preview_url, sign_key=upload_file_id, prefix="image-preview") - - @classmethod - def verify_image_file_signature(cls, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool: - """ - verify signature - - :param upload_file_id: file id - :param timestamp: timestamp - :param nonce: nonce - :param sign: signature - :return: - """ - result = UrlSigner.verify( - sign_key=upload_file_id, timestamp=timestamp, nonce=nonce, sign=sign, prefix="image-preview" - ) - - # verify signature - if not result: - return False - - current_time = int(time.time()) - return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT diff --git a/api/core/helper/code_executor/template_transformer.py b/api/core/helper/code_executor/template_transformer.py index baa792b5bc..b416e48ce4 100644 --- a/api/core/helper/code_executor/template_transformer.py +++ b/api/core/helper/code_executor/template_transformer.py @@ -5,6 +5,8 @@ from base64 import b64encode from collections.abc import Mapping from typing import Any +from core.variables.utils import SegmentJSONEncoder + class TemplateTransformer(ABC): _code_placeholder: str = "{{code}}" @@ -28,7 +30,7 @@ class TemplateTransformer(ABC): def extract_result_str_from_response(cls, response: str): result = re.search(rf"{cls._result_tag}(.*){cls._result_tag}", response, re.DOTALL) if not result: - raise ValueError("Failed to parse result") + raise ValueError(f"Failed to parse result: no result tag found in response. Response: {response[:200]}...") return result.group(1) @classmethod @@ -38,16 +40,49 @@ class TemplateTransformer(ABC): :param response: response :return: """ + try: - result = json.loads(cls.extract_result_str_from_response(response)) - except json.JSONDecodeError: - raise ValueError("failed to parse response") + result_str = cls.extract_result_str_from_response(response) + result = json.loads(result_str) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON response: {str(e)}.") + except ValueError as e: + # Re-raise ValueError from extract_result_str_from_response + raise e + except Exception as e: + raise ValueError(f"Unexpected error during response transformation: {str(e)}") + if not isinstance(result, dict): - raise ValueError("result must be a dict") + raise ValueError(f"Result must be a dict, got {type(result).__name__}") if not all(isinstance(k, str) for k in result): - raise ValueError("result keys must be strings") + raise ValueError("Result keys must be strings") + + # Post-process the result to convert scientific notation strings back to numbers + result = cls._post_process_result(result) return result + @classmethod + def _post_process_result(cls, result: dict[Any, Any]) -> dict[Any, Any]: + """ + Post-process the result to convert scientific notation strings back to numbers + """ + + def convert_scientific_notation(value): + if isinstance(value, str): + # Check if the string looks like scientific notation + if re.match(r"^-?\d+\.?\d*e[+-]\d+$", value, re.IGNORECASE): + try: + return float(value) + except ValueError: + pass + elif isinstance(value, dict): + return {k: convert_scientific_notation(v) for k, v in value.items()} + elif isinstance(value, list): + return [convert_scientific_notation(v) for v in value] + return value + + return convert_scientific_notation(result) # type: ignore[no-any-return] + @classmethod @abstractmethod def get_runner_script(cls) -> str: @@ -58,7 +93,7 @@ class TemplateTransformer(ABC): @classmethod def serialize_inputs(cls, inputs: Mapping[str, Any]) -> str: - inputs_json_str = json.dumps(inputs, ensure_ascii=False).encode() + inputs_json_str = json.dumps(inputs, ensure_ascii=False, cls=SegmentJSONEncoder).encode() input_base64_encoded = b64encode(inputs_json_str).decode("utf-8") return input_base64_encoded diff --git a/api/core/helper/lru_cache.py b/api/core/helper/lru_cache.py deleted file mode 100644 index 81501d2e4e..0000000000 --- a/api/core/helper/lru_cache.py +++ /dev/null @@ -1,22 +0,0 @@ -from collections import OrderedDict -from typing import Any - - -class LRUCache: - def __init__(self, capacity: int): - self.cache: OrderedDict[Any, Any] = OrderedDict() - self.capacity = capacity - - def get(self, key: Any) -> Any: - if key not in self.cache: - return None - else: - self.cache.move_to_end(key) # move the key to the end of the OrderedDict - return self.cache[key] - - def put(self, key: Any, value: Any) -> None: - if key in self.cache: - self.cache.move_to_end(key) - self.cache[key] = value - if len(self.cache) > self.capacity: - self.cache.popitem(last=False) # pop the first item diff --git a/api/core/helper/moderation.py b/api/core/helper/moderation.py index 6a5982eca4..a324ac2767 100644 --- a/api/core/helper/moderation.py +++ b/api/core/helper/moderation.py @@ -1,5 +1,5 @@ import logging -import random +import secrets from typing import cast from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity @@ -38,7 +38,7 @@ def check_moderation(tenant_id: str, model_config: ModelConfigWithCredentialsEnt if len(text_chunks) == 0: return True - text_chunk = random.choice(text_chunks) + text_chunk = secrets.choice(text_chunks) try: model_provider_factory = ModelProviderFactory(tenant_id) diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index 848d897779..305a9190d5 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -317,9 +317,10 @@ class IndexingRunner: image_upload_file_ids = get_image_upload_file_ids(document.page_content) for upload_file_id in image_upload_file_ids: image_file = db.session.query(UploadFile).filter(UploadFile.id == upload_file_id).first() + if image_file is None: + continue try: - if image_file: - storage.delete(image_file.key) + storage.delete(image_file.key) except Exception: logging.exception( "Delete image_files failed while indexing_estimate, \ @@ -534,7 +535,7 @@ class IndexingRunner: # chunk nodes by chunk size indexing_start_at = time.perf_counter() tokens = 0 - if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX: + if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX and dataset.indexing_technique == "economy": # create keyword index create_keyword_thread = threading.Thread( target=self._process_keyword_index, @@ -572,7 +573,7 @@ class IndexingRunner: for future in futures: tokens += future.result() - if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX: + if dataset_document.doc_form != IndexType.PARENT_CHILD_INDEX and dataset.indexing_technique == "economy": create_keyword_thread.join() indexing_end_at = time.perf_counter() diff --git a/api/core/llm_generator/output_parser/structured_output.py b/api/core/llm_generator/output_parser/structured_output.py new file mode 100644 index 0000000000..151cef1bc3 --- /dev/null +++ b/api/core/llm_generator/output_parser/structured_output.py @@ -0,0 +1,380 @@ +import json +from collections.abc import Generator, Mapping, Sequence +from copy import deepcopy +from enum import StrEnum +from typing import Any, Literal, Optional, cast, overload + +import json_repair +from pydantic import TypeAdapter, ValidationError + +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT +from core.model_manager import ModelInstance +from core.model_runtime.callbacks.base_callback import Callback +from core.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultChunkWithStructuredOutput, + LLMResultWithStructuredOutput, +) +from core.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + PromptMessage, + PromptMessageTool, + SystemPromptMessage, + TextPromptMessageContent, +) +from core.model_runtime.entities.model_entities import AIModelEntity, ParameterRule + + +class ResponseFormat(StrEnum): + """Constants for model response formats""" + + JSON_SCHEMA = "json_schema" # model's structured output mode. some model like gemini, gpt-4o, support this mode. + JSON = "JSON" # model's json mode. some model like claude support this mode. + JSON_OBJECT = "json_object" # json mode's another alias. some model like deepseek-chat, qwen use this alias. + + +class SpecialModelType(StrEnum): + """Constants for identifying model types""" + + GEMINI = "gemini" + OLLAMA = "ollama" + + +@overload +def invoke_llm_with_structured_output( + provider: str, + model_schema: AIModelEntity, + model_instance: ModelInstance, + prompt_messages: Sequence[PromptMessage], + json_schema: Mapping[str, Any], + model_parameters: Optional[Mapping] = None, + tools: Sequence[PromptMessageTool] | None = None, + stop: Optional[list[str]] = None, + stream: Literal[True] = True, + user: Optional[str] = None, + callbacks: Optional[list[Callback]] = None, +) -> Generator[LLMResultChunkWithStructuredOutput, None, None]: ... + + +@overload +def invoke_llm_with_structured_output( + provider: str, + model_schema: AIModelEntity, + model_instance: ModelInstance, + prompt_messages: Sequence[PromptMessage], + json_schema: Mapping[str, Any], + model_parameters: Optional[Mapping] = None, + tools: Sequence[PromptMessageTool] | None = None, + stop: Optional[list[str]] = None, + stream: Literal[False] = False, + user: Optional[str] = None, + callbacks: Optional[list[Callback]] = None, +) -> LLMResultWithStructuredOutput: ... + + +@overload +def invoke_llm_with_structured_output( + provider: str, + model_schema: AIModelEntity, + model_instance: ModelInstance, + prompt_messages: Sequence[PromptMessage], + json_schema: Mapping[str, Any], + model_parameters: Optional[Mapping] = None, + tools: Sequence[PromptMessageTool] | None = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + callbacks: Optional[list[Callback]] = None, +) -> LLMResultWithStructuredOutput | Generator[LLMResultChunkWithStructuredOutput, None, None]: ... + + +def invoke_llm_with_structured_output( + provider: str, + model_schema: AIModelEntity, + model_instance: ModelInstance, + prompt_messages: Sequence[PromptMessage], + json_schema: Mapping[str, Any], + model_parameters: Optional[Mapping] = None, + tools: Sequence[PromptMessageTool] | None = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + callbacks: Optional[list[Callback]] = None, +) -> LLMResultWithStructuredOutput | Generator[LLMResultChunkWithStructuredOutput, None, None]: + """ + Invoke large language model with structured output + 1. This method invokes model_instance.invoke_llm with json_schema + 2. Try to parse the result as structured output + + :param prompt_messages: prompt messages + :param json_schema: json schema + :param model_parameters: model parameters + :param tools: tools for tool calling + :param stop: stop words + :param stream: is stream response + :param user: unique user id + :param callbacks: callbacks + :return: full response or stream response chunk generator result + """ + + # handle native json schema + model_parameters_with_json_schema: dict[str, Any] = { + **(model_parameters or {}), + } + + if model_schema.support_structure_output: + model_parameters = _handle_native_json_schema( + provider, model_schema, json_schema, model_parameters_with_json_schema, model_schema.parameter_rules + ) + else: + # Set appropriate response format based on model capabilities + _set_response_format(model_parameters_with_json_schema, model_schema.parameter_rules) + + # handle prompt based schema + prompt_messages = _handle_prompt_based_schema( + prompt_messages=prompt_messages, + structured_output_schema=json_schema, + ) + + llm_result = model_instance.invoke_llm( + prompt_messages=list(prompt_messages), + model_parameters=model_parameters_with_json_schema, + tools=tools, + stop=stop, + stream=stream, + user=user, + callbacks=callbacks, + ) + + if isinstance(llm_result, LLMResult): + if not isinstance(llm_result.message.content, str): + raise OutputParserError( + f"Failed to parse structured output, LLM result is not a string: {llm_result.message.content}" + ) + + return LLMResultWithStructuredOutput( + structured_output=_parse_structured_output(llm_result.message.content), + model=llm_result.model, + message=llm_result.message, + usage=llm_result.usage, + system_fingerprint=llm_result.system_fingerprint, + prompt_messages=llm_result.prompt_messages, + ) + else: + + def generator() -> Generator[LLMResultChunkWithStructuredOutput, None, None]: + result_text: str = "" + prompt_messages: Sequence[PromptMessage] = [] + system_fingerprint: Optional[str] = None + for event in llm_result: + if isinstance(event, LLMResultChunk): + prompt_messages = event.prompt_messages + system_fingerprint = event.system_fingerprint + + if isinstance(event.delta.message.content, str): + result_text += event.delta.message.content + elif isinstance(event.delta.message.content, list): + for item in event.delta.message.content: + if isinstance(item, TextPromptMessageContent): + result_text += item.data + + yield LLMResultChunkWithStructuredOutput( + model=model_schema.model, + prompt_messages=prompt_messages, + system_fingerprint=system_fingerprint, + delta=event.delta, + ) + + yield LLMResultChunkWithStructuredOutput( + structured_output=_parse_structured_output(result_text), + model=model_schema.model, + prompt_messages=prompt_messages, + system_fingerprint=system_fingerprint, + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage(content=""), + usage=None, + finish_reason=None, + ), + ) + + return generator() + + +def _handle_native_json_schema( + provider: str, + model_schema: AIModelEntity, + structured_output_schema: Mapping, + model_parameters: dict, + rules: list[ParameterRule], +) -> dict: + """ + Handle structured output for models with native JSON schema support. + + :param model_parameters: Model parameters to update + :param rules: Model parameter rules + :return: Updated model parameters with JSON schema configuration + """ + # Process schema according to model requirements + schema_json = _prepare_schema_for_model(provider, model_schema, structured_output_schema) + + # Set JSON schema in parameters + model_parameters["json_schema"] = json.dumps(schema_json, ensure_ascii=False) + + # Set appropriate response format if required by the model + for rule in rules: + if rule.name == "response_format" and ResponseFormat.JSON_SCHEMA.value in rule.options: + model_parameters["response_format"] = ResponseFormat.JSON_SCHEMA.value + + return model_parameters + + +def _set_response_format(model_parameters: dict, rules: list) -> None: + """ + Set the appropriate response format parameter based on model rules. + + :param model_parameters: Model parameters to update + :param rules: Model parameter rules + """ + for rule in rules: + if rule.name == "response_format": + if ResponseFormat.JSON.value in rule.options: + model_parameters["response_format"] = ResponseFormat.JSON.value + elif ResponseFormat.JSON_OBJECT.value in rule.options: + model_parameters["response_format"] = ResponseFormat.JSON_OBJECT.value + + +def _handle_prompt_based_schema( + prompt_messages: Sequence[PromptMessage], structured_output_schema: Mapping +) -> list[PromptMessage]: + """ + Handle structured output for models without native JSON schema support. + This function modifies the prompt messages to include schema-based output requirements. + + Args: + prompt_messages: Original sequence of prompt messages + + Returns: + list[PromptMessage]: Updated prompt messages with structured output requirements + """ + # Convert schema to string format + schema_str = json.dumps(structured_output_schema, ensure_ascii=False) + + # Find existing system prompt with schema placeholder + system_prompt = next( + (prompt for prompt in prompt_messages if isinstance(prompt, SystemPromptMessage)), + None, + ) + structured_output_prompt = STRUCTURED_OUTPUT_PROMPT.replace("{{schema}}", schema_str) + # Prepare system prompt content + system_prompt_content = ( + structured_output_prompt + "\n\n" + system_prompt.content + if system_prompt and isinstance(system_prompt.content, str) + else structured_output_prompt + ) + system_prompt = SystemPromptMessage(content=system_prompt_content) + + # Extract content from the last user message + + filtered_prompts = [prompt for prompt in prompt_messages if not isinstance(prompt, SystemPromptMessage)] + updated_prompt = [system_prompt] + filtered_prompts + + return updated_prompt + + +def _parse_structured_output(result_text: str) -> Mapping[str, Any]: + structured_output: Mapping[str, Any] = {} + parsed: Mapping[str, Any] = {} + try: + parsed = TypeAdapter(Mapping).validate_json(result_text) + if not isinstance(parsed, dict): + raise OutputParserError(f"Failed to parse structured output: {result_text}") + structured_output = parsed + except ValidationError: + # if the result_text is not a valid json, try to repair it + temp_parsed = json_repair.loads(result_text) + if not isinstance(temp_parsed, dict): + # handle reasoning model like deepseek-r1 got '\n\n\n' prefix + if isinstance(temp_parsed, list): + temp_parsed = next((item for item in temp_parsed if isinstance(item, dict)), {}) + else: + raise OutputParserError(f"Failed to parse structured output: {result_text}") + structured_output = cast(dict, temp_parsed) + return structured_output + + +def _prepare_schema_for_model(provider: str, model_schema: AIModelEntity, schema: Mapping) -> dict: + """ + Prepare JSON schema based on model requirements. + + Different models have different requirements for JSON schema formatting. + This function handles these differences. + + :param schema: The original JSON schema + :return: Processed schema compatible with the current model + """ + + # Deep copy to avoid modifying the original schema + processed_schema = dict(deepcopy(schema)) + + # Convert boolean types to string types (common requirement) + convert_boolean_to_string(processed_schema) + + # Apply model-specific transformations + if SpecialModelType.GEMINI in model_schema.model: + remove_additional_properties(processed_schema) + return processed_schema + elif SpecialModelType.OLLAMA in provider: + return processed_schema + else: + # Default format with name field + return {"schema": processed_schema, "name": "llm_response"} + + +def remove_additional_properties(schema: dict) -> None: + """ + Remove additionalProperties fields from JSON schema. + Used for models like Gemini that don't support this property. + + :param schema: JSON schema to modify in-place + """ + if not isinstance(schema, dict): + return + + # Remove additionalProperties at current level + schema.pop("additionalProperties", None) + + # Process nested structures recursively + for value in schema.values(): + if isinstance(value, dict): + remove_additional_properties(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + remove_additional_properties(item) + + +def convert_boolean_to_string(schema: dict) -> None: + """ + Convert boolean type specifications to string in JSON schema. + + :param schema: JSON schema to modify in-place + """ + if not isinstance(schema, dict): + return + + # Check for boolean type at current level + if schema.get("type") == "boolean": + schema["type"] = "string" + + # Process nested dictionaries and lists recursively + for value in schema.values(): + if isinstance(value, dict): + convert_boolean_to_string(value) + elif isinstance(value, list): + for item in value: + if isinstance(item, dict): + convert_boolean_to_string(item) diff --git a/api/core/llm_generator/prompts.py b/api/core/llm_generator/prompts.py index ddfa1e7a66..ef81e38dc5 100644 --- a/api/core/llm_generator/prompts.py +++ b/api/core/llm_generator/prompts.py @@ -291,3 +291,21 @@ Your task is to convert simple user descriptions into properly formatted JSON Sc Now, generate a JSON Schema based on my description """ # noqa: E501 + +STRUCTURED_OUTPUT_PROMPT = """You’re a helpful AI assistant. You could answer questions and output in JSON format. +constraints: + - You must output in JSON format. + - Do not output boolean value, use string type instead. + - Do not output integer or float value, use number type instead. +eg: + Here is the JSON schema: + {"additionalProperties": false, "properties": {"age": {"type": "number"}, "name": {"type": "string"}}, "required": ["name", "age"], "type": "object"} + + Here is the user's question: + My name is John Doe and I am 30 years old. + + output: + {"name": "John Doe", "age": 30} +Here is the JSON schema: +{{schema}} +""" # noqa: E501 diff --git a/api/core/mcp/__init__.py b/api/core/mcp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/mcp/auth/auth_flow.py b/api/core/mcp/auth/auth_flow.py new file mode 100644 index 0000000000..bcb31a816f --- /dev/null +++ b/api/core/mcp/auth/auth_flow.py @@ -0,0 +1,342 @@ +import base64 +import hashlib +import json +import os +import secrets +import urllib.parse +from typing import Optional +from urllib.parse import urljoin + +import requests +from pydantic import BaseModel, ValidationError + +from core.mcp.auth.auth_provider import OAuthClientProvider +from core.mcp.types import ( + OAuthClientInformation, + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthMetadata, + OAuthTokens, +) +from extensions.ext_redis import redis_client + +LATEST_PROTOCOL_VERSION = "1.0" +OAUTH_STATE_EXPIRY_SECONDS = 5 * 60 # 5 minutes expiry +OAUTH_STATE_REDIS_KEY_PREFIX = "oauth_state:" + + +class OAuthCallbackState(BaseModel): + provider_id: str + tenant_id: str + server_url: str + metadata: OAuthMetadata | None = None + client_information: OAuthClientInformation + code_verifier: str + redirect_uri: str + + +def generate_pkce_challenge() -> tuple[str, str]: + """Generate PKCE challenge and verifier.""" + code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8") + code_verifier = code_verifier.replace("=", "").replace("+", "-").replace("/", "_") + + code_challenge_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest() + code_challenge = base64.urlsafe_b64encode(code_challenge_hash).decode("utf-8") + code_challenge = code_challenge.replace("=", "").replace("+", "-").replace("/", "_") + + return code_verifier, code_challenge + + +def _create_secure_redis_state(state_data: OAuthCallbackState) -> str: + """Create a secure state parameter by storing state data in Redis and returning a random state key.""" + # Generate a secure random state key + state_key = secrets.token_urlsafe(32) + + # Store the state data in Redis with expiration + redis_key = f"{OAUTH_STATE_REDIS_KEY_PREFIX}{state_key}" + redis_client.setex(redis_key, OAUTH_STATE_EXPIRY_SECONDS, state_data.model_dump_json()) + + return state_key + + +def _retrieve_redis_state(state_key: str) -> OAuthCallbackState: + """Retrieve and decode OAuth state data from Redis using the state key, then delete it.""" + redis_key = f"{OAUTH_STATE_REDIS_KEY_PREFIX}{state_key}" + + # Get state data from Redis + state_data = redis_client.get(redis_key) + + if not state_data: + raise ValueError("State parameter has expired or does not exist") + + # Delete the state data from Redis immediately after retrieval to prevent reuse + redis_client.delete(redis_key) + + try: + # Parse and validate the state data + oauth_state = OAuthCallbackState.model_validate_json(state_data) + + return oauth_state + except ValidationError as e: + raise ValueError(f"Invalid state parameter: {str(e)}") + + +def handle_callback(state_key: str, authorization_code: str) -> OAuthCallbackState: + """Handle the callback from the OAuth provider.""" + # Retrieve state data from Redis (state is automatically deleted after retrieval) + full_state_data = _retrieve_redis_state(state_key) + + tokens = exchange_authorization( + full_state_data.server_url, + full_state_data.metadata, + full_state_data.client_information, + authorization_code, + full_state_data.code_verifier, + full_state_data.redirect_uri, + ) + provider = OAuthClientProvider(full_state_data.provider_id, full_state_data.tenant_id, for_list=True) + provider.save_tokens(tokens) + return full_state_data + + +def discover_oauth_metadata(server_url: str, protocol_version: Optional[str] = None) -> Optional[OAuthMetadata]: + """Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.""" + url = urljoin(server_url, "/.well-known/oauth-authorization-server") + + try: + headers = {"MCP-Protocol-Version": protocol_version or LATEST_PROTOCOL_VERSION} + response = requests.get(url, headers=headers) + if response.status_code == 404: + return None + if not response.ok: + raise ValueError(f"HTTP {response.status_code} trying to load well-known OAuth metadata") + return OAuthMetadata.model_validate(response.json()) + except requests.RequestException as e: + if isinstance(e, requests.ConnectionError): + response = requests.get(url) + if response.status_code == 404: + return None + if not response.ok: + raise ValueError(f"HTTP {response.status_code} trying to load well-known OAuth metadata") + return OAuthMetadata.model_validate(response.json()) + raise + + +def start_authorization( + server_url: str, + metadata: Optional[OAuthMetadata], + client_information: OAuthClientInformation, + redirect_url: str, + provider_id: str, + tenant_id: str, +) -> tuple[str, str]: + """Begins the authorization flow with secure Redis state storage.""" + response_type = "code" + code_challenge_method = "S256" + + if metadata: + authorization_url = metadata.authorization_endpoint + if response_type not in metadata.response_types_supported: + raise ValueError(f"Incompatible auth server: does not support response type {response_type}") + if ( + not metadata.code_challenge_methods_supported + or code_challenge_method not in metadata.code_challenge_methods_supported + ): + raise ValueError( + f"Incompatible auth server: does not support code challenge method {code_challenge_method}" + ) + else: + authorization_url = urljoin(server_url, "/authorize") + + code_verifier, code_challenge = generate_pkce_challenge() + + # Prepare state data with all necessary information + state_data = OAuthCallbackState( + provider_id=provider_id, + tenant_id=tenant_id, + server_url=server_url, + metadata=metadata, + client_information=client_information, + code_verifier=code_verifier, + redirect_uri=redirect_url, + ) + + # Store state data in Redis and generate secure state key + state_key = _create_secure_redis_state(state_data) + + params = { + "response_type": response_type, + "client_id": client_information.client_id, + "code_challenge": code_challenge, + "code_challenge_method": code_challenge_method, + "redirect_uri": redirect_url, + "state": state_key, + } + + authorization_url = f"{authorization_url}?{urllib.parse.urlencode(params)}" + return authorization_url, code_verifier + + +def exchange_authorization( + server_url: str, + metadata: Optional[OAuthMetadata], + client_information: OAuthClientInformation, + authorization_code: str, + code_verifier: str, + redirect_uri: str, +) -> OAuthTokens: + """Exchanges an authorization code for an access token.""" + grant_type = "authorization_code" + + if metadata: + token_url = metadata.token_endpoint + if metadata.grant_types_supported and grant_type not in metadata.grant_types_supported: + raise ValueError(f"Incompatible auth server: does not support grant type {grant_type}") + else: + token_url = urljoin(server_url, "/token") + + params = { + "grant_type": grant_type, + "client_id": client_information.client_id, + "code": authorization_code, + "code_verifier": code_verifier, + "redirect_uri": redirect_uri, + } + + if client_information.client_secret: + params["client_secret"] = client_information.client_secret + + response = requests.post(token_url, data=params) + if not response.ok: + raise ValueError(f"Token exchange failed: HTTP {response.status_code}") + return OAuthTokens.model_validate(response.json()) + + +def refresh_authorization( + server_url: str, + metadata: Optional[OAuthMetadata], + client_information: OAuthClientInformation, + refresh_token: str, +) -> OAuthTokens: + """Exchange a refresh token for an updated access token.""" + grant_type = "refresh_token" + + if metadata: + token_url = metadata.token_endpoint + if metadata.grant_types_supported and grant_type not in metadata.grant_types_supported: + raise ValueError(f"Incompatible auth server: does not support grant type {grant_type}") + else: + token_url = urljoin(server_url, "/token") + + params = { + "grant_type": grant_type, + "client_id": client_information.client_id, + "refresh_token": refresh_token, + } + + if client_information.client_secret: + params["client_secret"] = client_information.client_secret + + response = requests.post(token_url, data=params) + if not response.ok: + raise ValueError(f"Token refresh failed: HTTP {response.status_code}") + return OAuthTokens.model_validate(response.json()) + + +def register_client( + server_url: str, + metadata: Optional[OAuthMetadata], + client_metadata: OAuthClientMetadata, +) -> OAuthClientInformationFull: + """Performs OAuth 2.0 Dynamic Client Registration.""" + if metadata: + if not metadata.registration_endpoint: + raise ValueError("Incompatible auth server: does not support dynamic client registration") + registration_url = metadata.registration_endpoint + else: + registration_url = urljoin(server_url, "/register") + + response = requests.post( + registration_url, + json=client_metadata.model_dump(), + headers={"Content-Type": "application/json"}, + ) + if not response.ok: + response.raise_for_status() + return OAuthClientInformationFull.model_validate(response.json()) + + +def auth( + provider: OAuthClientProvider, + server_url: str, + authorization_code: Optional[str] = None, + state_param: Optional[str] = None, + for_list: bool = False, +) -> dict[str, str]: + """Orchestrates the full auth flow with a server using secure Redis state storage.""" + metadata = discover_oauth_metadata(server_url) + + # Handle client registration if needed + client_information = provider.client_information() + if not client_information: + if authorization_code is not None: + raise ValueError("Existing OAuth client information is required when exchanging an authorization code") + try: + full_information = register_client(server_url, metadata, provider.client_metadata) + except requests.RequestException as e: + raise ValueError(f"Could not register OAuth client: {e}") + provider.save_client_information(full_information) + client_information = full_information + + # Exchange authorization code for tokens + if authorization_code is not None: + if not state_param: + raise ValueError("State parameter is required when exchanging authorization code") + + try: + # Retrieve state data from Redis using state key + full_state_data = _retrieve_redis_state(state_param) + + code_verifier = full_state_data.code_verifier + redirect_uri = full_state_data.redirect_uri + + if not code_verifier or not redirect_uri: + raise ValueError("Missing code_verifier or redirect_uri in state data") + + except (json.JSONDecodeError, ValueError) as e: + raise ValueError(f"Invalid state parameter: {e}") + + tokens = exchange_authorization( + server_url, + metadata, + client_information, + authorization_code, + code_verifier, + redirect_uri, + ) + provider.save_tokens(tokens) + return {"result": "success"} + + provider_tokens = provider.tokens() + + # Handle token refresh or new authorization + if provider_tokens and provider_tokens.refresh_token: + try: + new_tokens = refresh_authorization(server_url, metadata, client_information, provider_tokens.refresh_token) + provider.save_tokens(new_tokens) + return {"result": "success"} + except Exception as e: + raise ValueError(f"Could not refresh OAuth tokens: {e}") + + # Start new authorization flow + authorization_url, code_verifier = start_authorization( + server_url, + metadata, + client_information, + provider.redirect_url, + provider.mcp_provider.id, + provider.mcp_provider.tenant_id, + ) + + provider.save_code_verifier(code_verifier) + return {"authorization_url": authorization_url} diff --git a/api/core/mcp/auth/auth_provider.py b/api/core/mcp/auth/auth_provider.py new file mode 100644 index 0000000000..cd55dbf64f --- /dev/null +++ b/api/core/mcp/auth/auth_provider.py @@ -0,0 +1,81 @@ +from typing import Optional + +from configs import dify_config +from core.mcp.types import ( + OAuthClientInformation, + OAuthClientInformationFull, + OAuthClientMetadata, + OAuthTokens, +) +from models.tools import MCPToolProvider +from services.tools.mcp_tools_mange_service import MCPToolManageService + +LATEST_PROTOCOL_VERSION = "1.0" + + +class OAuthClientProvider: + mcp_provider: MCPToolProvider + + def __init__(self, provider_id: str, tenant_id: str, for_list: bool = False): + if for_list: + self.mcp_provider = MCPToolManageService.get_mcp_provider_by_provider_id(provider_id, tenant_id) + else: + self.mcp_provider = MCPToolManageService.get_mcp_provider_by_server_identifier(provider_id, tenant_id) + + @property + def redirect_url(self) -> str: + """The URL to redirect the user agent to after authorization.""" + return dify_config.CONSOLE_API_URL + "/console/api/mcp/oauth/callback" + + @property + def client_metadata(self) -> OAuthClientMetadata: + """Metadata about this OAuth client.""" + return OAuthClientMetadata( + redirect_uris=[self.redirect_url], + token_endpoint_auth_method="none", + grant_types=["authorization_code", "refresh_token"], + response_types=["code"], + client_name="Dify", + client_uri="https://github.com/langgenius/dify", + ) + + def client_information(self) -> Optional[OAuthClientInformation]: + """Loads information about this OAuth client.""" + client_information = self.mcp_provider.decrypted_credentials.get("client_information", {}) + if not client_information: + return None + return OAuthClientInformation.model_validate(client_information) + + def save_client_information(self, client_information: OAuthClientInformationFull) -> None: + """Saves client information after dynamic registration.""" + MCPToolManageService.update_mcp_provider_credentials( + self.mcp_provider, + {"client_information": client_information.model_dump()}, + ) + + def tokens(self) -> Optional[OAuthTokens]: + """Loads any existing OAuth tokens for the current session.""" + credentials = self.mcp_provider.decrypted_credentials + if not credentials: + return None + return OAuthTokens( + access_token=credentials.get("access_token", ""), + token_type=credentials.get("token_type", "Bearer"), + expires_in=int(credentials.get("expires_in", "3600") or 3600), + refresh_token=credentials.get("refresh_token", ""), + ) + + def save_tokens(self, tokens: OAuthTokens) -> None: + """Stores new OAuth tokens for the current session.""" + # update mcp provider credentials + token_dict = tokens.model_dump() + MCPToolManageService.update_mcp_provider_credentials(self.mcp_provider, token_dict, authed=True) + + def save_code_verifier(self, code_verifier: str) -> None: + """Saves a PKCE code verifier for the current session.""" + MCPToolManageService.update_mcp_provider_credentials(self.mcp_provider, {"code_verifier": code_verifier}) + + def code_verifier(self) -> str: + """Loads the PKCE code verifier for the current session.""" + # get code verifier from mcp provider credentials + return str(self.mcp_provider.decrypted_credentials.get("code_verifier", "")) diff --git a/api/core/mcp/client/sse_client.py b/api/core/mcp/client/sse_client.py new file mode 100644 index 0000000000..91debcc8f9 --- /dev/null +++ b/api/core/mcp/client/sse_client.py @@ -0,0 +1,361 @@ +import logging +import queue +from collections.abc import Generator +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from typing import Any, TypeAlias, final +from urllib.parse import urljoin, urlparse + +import httpx +from sseclient import SSEClient + +from core.mcp import types +from core.mcp.error import MCPAuthError, MCPConnectionError +from core.mcp.types import SessionMessage +from core.mcp.utils import create_ssrf_proxy_mcp_http_client, ssrf_proxy_sse_connect + +logger = logging.getLogger(__name__) + +DEFAULT_QUEUE_READ_TIMEOUT = 3 + + +@final +class _StatusReady: + def __init__(self, endpoint_url: str): + self._endpoint_url = endpoint_url + + +@final +class _StatusError: + def __init__(self, exc: Exception): + self._exc = exc + + +# Type aliases for better readability +ReadQueue: TypeAlias = queue.Queue[SessionMessage | Exception | None] +WriteQueue: TypeAlias = queue.Queue[SessionMessage | Exception | None] +StatusQueue: TypeAlias = queue.Queue[_StatusReady | _StatusError] + + +def remove_request_params(url: str) -> str: + """Remove request parameters from URL, keeping only the path.""" + return urljoin(url, urlparse(url).path) + + +class SSETransport: + """SSE client transport implementation.""" + + def __init__( + self, + url: str, + headers: dict[str, Any] | None = None, + timeout: float = 5.0, + sse_read_timeout: float = 5 * 60, + ) -> None: + """Initialize the SSE transport. + + Args: + url: The SSE endpoint URL. + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + """ + self.url = url + self.headers = headers or {} + self.timeout = timeout + self.sse_read_timeout = sse_read_timeout + self.endpoint_url: str | None = None + + def _validate_endpoint_url(self, endpoint_url: str) -> bool: + """Validate that the endpoint URL matches the connection origin. + + Args: + endpoint_url: The endpoint URL to validate. + + Returns: + True if valid, False otherwise. + """ + url_parsed = urlparse(self.url) + endpoint_parsed = urlparse(endpoint_url) + + return url_parsed.netloc == endpoint_parsed.netloc and url_parsed.scheme == endpoint_parsed.scheme + + def _handle_endpoint_event(self, sse_data: str, status_queue: StatusQueue) -> None: + """Handle an 'endpoint' SSE event. + + Args: + sse_data: The SSE event data. + status_queue: Queue to put status updates. + """ + endpoint_url = urljoin(self.url, sse_data) + logger.info(f"Received endpoint URL: {endpoint_url}") + + if not self._validate_endpoint_url(endpoint_url): + error_msg = f"Endpoint origin does not match connection origin: {endpoint_url}" + logger.error(error_msg) + status_queue.put(_StatusError(ValueError(error_msg))) + return + + status_queue.put(_StatusReady(endpoint_url)) + + def _handle_message_event(self, sse_data: str, read_queue: ReadQueue) -> None: + """Handle a 'message' SSE event. + + Args: + sse_data: The SSE event data. + read_queue: Queue to put parsed messages. + """ + try: + message = types.JSONRPCMessage.model_validate_json(sse_data) + logger.debug(f"Received server message: {message}") + session_message = SessionMessage(message) + read_queue.put(session_message) + except Exception as exc: + logger.exception("Error parsing server message") + read_queue.put(exc) + + def _handle_sse_event(self, sse, read_queue: ReadQueue, status_queue: StatusQueue) -> None: + """Handle a single SSE event. + + Args: + sse: The SSE event object. + read_queue: Queue for message events. + status_queue: Queue for status events. + """ + match sse.event: + case "endpoint": + self._handle_endpoint_event(sse.data, status_queue) + case "message": + self._handle_message_event(sse.data, read_queue) + case _: + logger.warning(f"Unknown SSE event: {sse.event}") + + def sse_reader(self, event_source, read_queue: ReadQueue, status_queue: StatusQueue) -> None: + """Read and process SSE events. + + Args: + event_source: The SSE event source. + read_queue: Queue to put received messages. + status_queue: Queue to put status updates. + """ + try: + for sse in event_source.iter_sse(): + self._handle_sse_event(sse, read_queue, status_queue) + except httpx.ReadError as exc: + logger.debug(f"SSE reader shutting down normally: {exc}") + except Exception as exc: + read_queue.put(exc) + finally: + read_queue.put(None) + + def _send_message(self, client: httpx.Client, endpoint_url: str, message: SessionMessage) -> None: + """Send a single message to the server. + + Args: + client: HTTP client to use. + endpoint_url: The endpoint URL to send to. + message: The message to send. + """ + response = client.post( + endpoint_url, + json=message.message.model_dump( + by_alias=True, + mode="json", + exclude_none=True, + ), + ) + response.raise_for_status() + logger.debug(f"Client message sent successfully: {response.status_code}") + + def post_writer(self, client: httpx.Client, endpoint_url: str, write_queue: WriteQueue) -> None: + """Handle writing messages to the server. + + Args: + client: HTTP client to use. + endpoint_url: The endpoint URL to send messages to. + write_queue: Queue to read messages from. + """ + try: + while True: + try: + message = write_queue.get(timeout=DEFAULT_QUEUE_READ_TIMEOUT) + if message is None: + break + if isinstance(message, Exception): + write_queue.put(message) + continue + + self._send_message(client, endpoint_url, message) + + except queue.Empty: + continue + except httpx.ReadError as exc: + logger.debug(f"Post writer shutting down normally: {exc}") + except Exception as exc: + logger.exception("Error writing messages") + write_queue.put(exc) + finally: + write_queue.put(None) + + def _wait_for_endpoint(self, status_queue: StatusQueue) -> str: + """Wait for the endpoint URL from the status queue. + + Args: + status_queue: Queue to read status from. + + Returns: + The endpoint URL. + + Raises: + ValueError: If endpoint URL is not received or there's an error. + """ + try: + status = status_queue.get(timeout=1) + except queue.Empty: + raise ValueError("failed to get endpoint URL") + + if isinstance(status, _StatusReady): + return status._endpoint_url + elif isinstance(status, _StatusError): + raise status._exc + else: + raise ValueError("failed to get endpoint URL") + + def connect( + self, + executor: ThreadPoolExecutor, + client: httpx.Client, + event_source, + ) -> tuple[ReadQueue, WriteQueue]: + """Establish connection and start worker threads. + + Args: + executor: Thread pool executor. + client: HTTP client. + event_source: SSE event source. + + Returns: + Tuple of (read_queue, write_queue). + """ + read_queue: ReadQueue = queue.Queue() + write_queue: WriteQueue = queue.Queue() + status_queue: StatusQueue = queue.Queue() + + # Start SSE reader thread + executor.submit(self.sse_reader, event_source, read_queue, status_queue) + + # Wait for endpoint URL + endpoint_url = self._wait_for_endpoint(status_queue) + self.endpoint_url = endpoint_url + + # Start post writer thread + executor.submit(self.post_writer, client, endpoint_url, write_queue) + + return read_queue, write_queue + + +@contextmanager +def sse_client( + url: str, + headers: dict[str, Any] | None = None, + timeout: float = 5.0, + sse_read_timeout: float = 5 * 60, +) -> Generator[tuple[ReadQueue, WriteQueue], None, None]: + """ + Client transport for SSE. + `sse_read_timeout` determines how long (in seconds) the client will wait for a new + event before disconnecting. All other HTTP operations are controlled by `timeout`. + + Args: + url: The SSE endpoint URL. + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + + Yields: + Tuple of (read_queue, write_queue) for message communication. + """ + transport = SSETransport(url, headers, timeout, sse_read_timeout) + + read_queue: ReadQueue | None = None + write_queue: WriteQueue | None = None + + with ThreadPoolExecutor() as executor: + try: + with create_ssrf_proxy_mcp_http_client(headers=transport.headers) as client: + with ssrf_proxy_sse_connect( + url, timeout=httpx.Timeout(timeout, read=sse_read_timeout), client=client + ) as event_source: + event_source.response.raise_for_status() + + read_queue, write_queue = transport.connect(executor, client, event_source) + + yield read_queue, write_queue + + except httpx.HTTPStatusError as exc: + if exc.response.status_code == 401: + raise MCPAuthError() + raise MCPConnectionError() + except Exception: + logger.exception("Error connecting to SSE endpoint") + raise + finally: + # Clean up queues + if read_queue: + read_queue.put(None) + if write_queue: + write_queue.put(None) + + +def send_message(http_client: httpx.Client, endpoint_url: str, session_message: SessionMessage) -> None: + """ + Send a message to the server using the provided HTTP client. + + Args: + http_client: The HTTP client to use for sending + endpoint_url: The endpoint URL to send the message to + session_message: The message to send + """ + try: + response = http_client.post( + endpoint_url, + json=session_message.message.model_dump( + by_alias=True, + mode="json", + exclude_none=True, + ), + ) + response.raise_for_status() + logger.debug(f"Client message sent successfully: {response.status_code}") + except Exception as exc: + logger.exception("Error sending message") + raise + + +def read_messages( + sse_client: SSEClient, +) -> Generator[SessionMessage | Exception, None, None]: + """ + Read messages from the SSE client. + + Args: + sse_client: The SSE client to read from + + Yields: + SessionMessage or Exception for each event received + """ + try: + for sse in sse_client.events(): + if sse.event == "message": + try: + message = types.JSONRPCMessage.model_validate_json(sse.data) + logger.debug(f"Received server message: {message}") + yield SessionMessage(message) + except Exception as exc: + logger.exception("Error parsing server message") + yield exc + else: + logger.warning(f"Unknown SSE event: {sse.event}") + except Exception as exc: + logger.exception("Error reading SSE messages") + yield exc diff --git a/api/core/mcp/client/streamable_client.py b/api/core/mcp/client/streamable_client.py new file mode 100644 index 0000000000..fbd8d05f9e --- /dev/null +++ b/api/core/mcp/client/streamable_client.py @@ -0,0 +1,476 @@ +""" +StreamableHTTP Client Transport Module + +This module implements the StreamableHTTP transport for MCP clients, +providing support for HTTP POST requests with optional SSE streaming responses +and session management. +""" + +import logging +import queue +from collections.abc import Callable, Generator +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, cast + +import httpx +from httpx_sse import EventSource, ServerSentEvent + +from core.mcp.types import ( + ClientMessageMetadata, + ErrorData, + JSONRPCError, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + RequestId, + SessionMessage, +) +from core.mcp.utils import create_ssrf_proxy_mcp_http_client, ssrf_proxy_sse_connect + +logger = logging.getLogger(__name__) + + +SessionMessageOrError = SessionMessage | Exception | None +# Queue types with clearer names for their roles +ServerToClientQueue = queue.Queue[SessionMessageOrError] # Server to client messages +ClientToServerQueue = queue.Queue[SessionMessage | None] # Client to server messages +GetSessionIdCallback = Callable[[], str | None] + +MCP_SESSION_ID = "mcp-session-id" +LAST_EVENT_ID = "last-event-id" +CONTENT_TYPE = "content-type" +ACCEPT = "Accept" + + +JSON = "application/json" +SSE = "text/event-stream" + +DEFAULT_QUEUE_READ_TIMEOUT = 3 + + +class StreamableHTTPError(Exception): + """Base exception for StreamableHTTP transport errors.""" + + pass + + +class ResumptionError(StreamableHTTPError): + """Raised when resumption request is invalid.""" + + pass + + +@dataclass +class RequestContext: + """Context for a request operation.""" + + client: httpx.Client + headers: dict[str, str] + session_id: str | None + session_message: SessionMessage + metadata: ClientMessageMetadata | None + server_to_client_queue: ServerToClientQueue # Renamed for clarity + sse_read_timeout: timedelta + + +class StreamableHTTPTransport: + """StreamableHTTP client transport implementation.""" + + def __init__( + self, + url: str, + headers: dict[str, Any] | None = None, + timeout: timedelta = timedelta(seconds=30), + sse_read_timeout: timedelta = timedelta(seconds=60 * 5), + ) -> None: + """Initialize the StreamableHTTP transport. + + Args: + url: The endpoint URL. + headers: Optional headers to include in requests. + timeout: HTTP timeout for regular operations. + sse_read_timeout: Timeout for SSE read operations. + """ + self.url = url + self.headers = headers or {} + self.timeout = timeout + self.sse_read_timeout = sse_read_timeout + self.session_id: str | None = None + self.request_headers = { + ACCEPT: f"{JSON}, {SSE}", + CONTENT_TYPE: JSON, + **self.headers, + } + + def _update_headers_with_session(self, base_headers: dict[str, str]) -> dict[str, str]: + """Update headers with session ID if available.""" + headers = base_headers.copy() + if self.session_id: + headers[MCP_SESSION_ID] = self.session_id + return headers + + def _is_initialization_request(self, message: JSONRPCMessage) -> bool: + """Check if the message is an initialization request.""" + return isinstance(message.root, JSONRPCRequest) and message.root.method == "initialize" + + def _is_initialized_notification(self, message: JSONRPCMessage) -> bool: + """Check if the message is an initialized notification.""" + return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized" + + def _maybe_extract_session_id_from_response( + self, + response: httpx.Response, + ) -> None: + """Extract and store session ID from response headers.""" + new_session_id = response.headers.get(MCP_SESSION_ID) + if new_session_id: + self.session_id = new_session_id + logger.info(f"Received session ID: {self.session_id}") + + def _handle_sse_event( + self, + sse: ServerSentEvent, + server_to_client_queue: ServerToClientQueue, + original_request_id: RequestId | None = None, + resumption_callback: Callable[[str], None] | None = None, + ) -> bool: + """Handle an SSE event, returning True if the response is complete.""" + if sse.event == "message": + try: + message = JSONRPCMessage.model_validate_json(sse.data) + logger.debug(f"SSE message: {message}") + + # If this is a response and we have original_request_id, replace it + if original_request_id is not None and isinstance(message.root, JSONRPCResponse | JSONRPCError): + message.root.id = original_request_id + + session_message = SessionMessage(message) + # Put message in queue that goes to client + server_to_client_queue.put(session_message) + + # Call resumption token callback if we have an ID + if sse.id and resumption_callback: + resumption_callback(sse.id) + + # If this is a response or error return True indicating completion + # Otherwise, return False to continue listening + return isinstance(message.root, JSONRPCResponse | JSONRPCError) + + except Exception as exc: + # Put exception in queue that goes to client + server_to_client_queue.put(exc) + return False + elif sse.event == "ping": + logger.debug("Received ping event") + return False + else: + logger.warning(f"Unknown SSE event: {sse.event}") + return False + + def handle_get_stream( + self, + client: httpx.Client, + server_to_client_queue: ServerToClientQueue, + ) -> None: + """Handle GET stream for server-initiated messages.""" + try: + if not self.session_id: + return + + headers = self._update_headers_with_session(self.request_headers) + + with ssrf_proxy_sse_connect( + self.url, + headers=headers, + timeout=httpx.Timeout(self.timeout.seconds, read=self.sse_read_timeout.seconds), + client=client, + method="GET", + ) as event_source: + event_source.response.raise_for_status() + logger.debug("GET SSE connection established") + + for sse in event_source.iter_sse(): + self._handle_sse_event(sse, server_to_client_queue) + + except Exception as exc: + logger.debug(f"GET stream error (non-fatal): {exc}") + + def _handle_resumption_request(self, ctx: RequestContext) -> None: + """Handle a resumption request using GET with SSE.""" + headers = self._update_headers_with_session(ctx.headers) + if ctx.metadata and ctx.metadata.resumption_token: + headers[LAST_EVENT_ID] = ctx.metadata.resumption_token + else: + raise ResumptionError("Resumption request requires a resumption token") + + # Extract original request ID to map responses + original_request_id = None + if isinstance(ctx.session_message.message.root, JSONRPCRequest): + original_request_id = ctx.session_message.message.root.id + + with ssrf_proxy_sse_connect( + self.url, + headers=headers, + timeout=httpx.Timeout(self.timeout.seconds, read=ctx.sse_read_timeout.seconds), + client=ctx.client, + method="GET", + ) as event_source: + event_source.response.raise_for_status() + logger.debug("Resumption GET SSE connection established") + + for sse in event_source.iter_sse(): + is_complete = self._handle_sse_event( + sse, + ctx.server_to_client_queue, + original_request_id, + ctx.metadata.on_resumption_token_update if ctx.metadata else None, + ) + if is_complete: + break + + def _handle_post_request(self, ctx: RequestContext) -> None: + """Handle a POST request with response processing.""" + headers = self._update_headers_with_session(ctx.headers) + message = ctx.session_message.message + is_initialization = self._is_initialization_request(message) + + with ctx.client.stream( + "POST", + self.url, + json=message.model_dump(by_alias=True, mode="json", exclude_none=True), + headers=headers, + ) as response: + if response.status_code == 202: + logger.debug("Received 202 Accepted") + return + + if response.status_code == 404: + if isinstance(message.root, JSONRPCRequest): + self._send_session_terminated_error( + ctx.server_to_client_queue, + message.root.id, + ) + return + + response.raise_for_status() + if is_initialization: + self._maybe_extract_session_id_from_response(response) + + content_type = cast(str, response.headers.get(CONTENT_TYPE, "").lower()) + + if content_type.startswith(JSON): + self._handle_json_response(response, ctx.server_to_client_queue) + elif content_type.startswith(SSE): + self._handle_sse_response(response, ctx) + else: + self._handle_unexpected_content_type( + content_type, + ctx.server_to_client_queue, + ) + + def _handle_json_response( + self, + response: httpx.Response, + server_to_client_queue: ServerToClientQueue, + ) -> None: + """Handle JSON response from the server.""" + try: + content = response.read() + message = JSONRPCMessage.model_validate_json(content) + session_message = SessionMessage(message) + server_to_client_queue.put(session_message) + except Exception as exc: + server_to_client_queue.put(exc) + + def _handle_sse_response(self, response: httpx.Response, ctx: RequestContext) -> None: + """Handle SSE response from the server.""" + try: + event_source = EventSource(response) + for sse in event_source.iter_sse(): + is_complete = self._handle_sse_event( + sse, + ctx.server_to_client_queue, + resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None), + ) + if is_complete: + break + except Exception as e: + ctx.server_to_client_queue.put(e) + + def _handle_unexpected_content_type( + self, + content_type: str, + server_to_client_queue: ServerToClientQueue, + ) -> None: + """Handle unexpected content type in response.""" + error_msg = f"Unexpected content type: {content_type}" + logger.error(error_msg) + server_to_client_queue.put(ValueError(error_msg)) + + def _send_session_terminated_error( + self, + server_to_client_queue: ServerToClientQueue, + request_id: RequestId, + ) -> None: + """Send a session terminated error response.""" + jsonrpc_error = JSONRPCError( + jsonrpc="2.0", + id=request_id, + error=ErrorData(code=32600, message="Session terminated by server"), + ) + session_message = SessionMessage(JSONRPCMessage(jsonrpc_error)) + server_to_client_queue.put(session_message) + + def post_writer( + self, + client: httpx.Client, + client_to_server_queue: ClientToServerQueue, + server_to_client_queue: ServerToClientQueue, + start_get_stream: Callable[[], None], + ) -> None: + """Handle writing requests to the server. + + This method processes messages from the client_to_server_queue and sends them to the server. + Responses are written to the server_to_client_queue. + """ + while True: + try: + # Read message from client queue with timeout to check stop_event periodically + session_message = client_to_server_queue.get(timeout=DEFAULT_QUEUE_READ_TIMEOUT) + if session_message is None: + break + + message = session_message.message + metadata = ( + session_message.metadata if isinstance(session_message.metadata, ClientMessageMetadata) else None + ) + + # Check if this is a resumption request + is_resumption = bool(metadata and metadata.resumption_token) + + logger.debug(f"Sending client message: {message}") + + # Handle initialized notification + if self._is_initialized_notification(message): + start_get_stream() + + ctx = RequestContext( + client=client, + headers=self.request_headers, + session_id=self.session_id, + session_message=session_message, + metadata=metadata, + server_to_client_queue=server_to_client_queue, # Queue to write responses to client + sse_read_timeout=self.sse_read_timeout, + ) + + if is_resumption: + self._handle_resumption_request(ctx) + else: + self._handle_post_request(ctx) + except queue.Empty: + continue + except Exception as exc: + server_to_client_queue.put(exc) + + def terminate_session(self, client: httpx.Client) -> None: + """Terminate the session by sending a DELETE request.""" + if not self.session_id: + return + + try: + headers = self._update_headers_with_session(self.request_headers) + response = client.delete(self.url, headers=headers) + + if response.status_code == 405: + logger.debug("Server does not allow session termination") + elif response.status_code != 200: + logger.warning(f"Session termination failed: {response.status_code}") + except Exception as exc: + logger.warning(f"Session termination failed: {exc}") + + def get_session_id(self) -> str | None: + """Get the current session ID.""" + return self.session_id + + +@contextmanager +def streamablehttp_client( + url: str, + headers: dict[str, Any] | None = None, + timeout: timedelta = timedelta(seconds=30), + sse_read_timeout: timedelta = timedelta(seconds=60 * 5), + terminate_on_close: bool = True, +) -> Generator[ + tuple[ + ServerToClientQueue, # Queue for receiving messages FROM server + ClientToServerQueue, # Queue for sending messages TO server + GetSessionIdCallback, + ], + None, + None, +]: + """ + Client transport for StreamableHTTP. + + `sse_read_timeout` determines how long (in seconds) the client will wait for a new + event before disconnecting. All other HTTP operations are controlled by `timeout`. + + Yields: + Tuple containing: + - server_to_client_queue: Queue for reading messages FROM the server + - client_to_server_queue: Queue for sending messages TO the server + - get_session_id_callback: Function to retrieve the current session ID + """ + transport = StreamableHTTPTransport(url, headers, timeout, sse_read_timeout) + + # Create queues with clear directional meaning + server_to_client_queue: ServerToClientQueue = queue.Queue() # For messages FROM server TO client + client_to_server_queue: ClientToServerQueue = queue.Queue() # For messages FROM client TO server + + with ThreadPoolExecutor(max_workers=2) as executor: + try: + with create_ssrf_proxy_mcp_http_client( + headers=transport.request_headers, + timeout=httpx.Timeout(transport.timeout.seconds, read=transport.sse_read_timeout.seconds), + ) as client: + # Define callbacks that need access to thread pool + def start_get_stream() -> None: + """Start a worker thread to handle server-initiated messages.""" + executor.submit(transport.handle_get_stream, client, server_to_client_queue) + + # Start the post_writer worker thread + executor.submit( + transport.post_writer, + client, + client_to_server_queue, # Queue for messages FROM client TO server + server_to_client_queue, # Queue for messages FROM server TO client + start_get_stream, + ) + + try: + yield ( + server_to_client_queue, # Queue for receiving messages FROM server + client_to_server_queue, # Queue for sending messages TO server + transport.get_session_id, + ) + finally: + if transport.session_id and terminate_on_close: + transport.terminate_session(client) + + # Signal threads to stop + client_to_server_queue.put(None) + finally: + # Clear any remaining items and add None sentinel to unblock any waiting threads + try: + while not client_to_server_queue.empty(): + client_to_server_queue.get_nowait() + except queue.Empty: + pass + + client_to_server_queue.put(None) + server_to_client_queue.put(None) diff --git a/api/core/mcp/entities.py b/api/core/mcp/entities.py new file mode 100644 index 0000000000..7553c10a2e --- /dev/null +++ b/api/core/mcp/entities.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from core.mcp.session.base_session import BaseSession +from core.mcp.types import LATEST_PROTOCOL_VERSION, RequestId, RequestParams + +SUPPORTED_PROTOCOL_VERSIONS: list[str] = ["2024-11-05", LATEST_PROTOCOL_VERSION] + + +SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any]) +LifespanContextT = TypeVar("LifespanContextT") + + +@dataclass +class RequestContext(Generic[SessionT, LifespanContextT]): + request_id: RequestId + meta: RequestParams.Meta | None + session: SessionT + lifespan_context: LifespanContextT diff --git a/api/core/mcp/error.py b/api/core/mcp/error.py new file mode 100644 index 0000000000..92ea7bde09 --- /dev/null +++ b/api/core/mcp/error.py @@ -0,0 +1,10 @@ +class MCPError(Exception): + pass + + +class MCPConnectionError(MCPError): + pass + + +class MCPAuthError(MCPConnectionError): + pass diff --git a/api/core/mcp/mcp_client.py b/api/core/mcp/mcp_client.py new file mode 100644 index 0000000000..e9036de8c6 --- /dev/null +++ b/api/core/mcp/mcp_client.py @@ -0,0 +1,150 @@ +import logging +from collections.abc import Callable +from contextlib import AbstractContextManager, ExitStack +from types import TracebackType +from typing import Any, Optional, cast +from urllib.parse import urlparse + +from core.mcp.client.sse_client import sse_client +from core.mcp.client.streamable_client import streamablehttp_client +from core.mcp.error import MCPAuthError, MCPConnectionError +from core.mcp.session.client_session import ClientSession +from core.mcp.types import Tool + +logger = logging.getLogger(__name__) + + +class MCPClient: + def __init__( + self, + server_url: str, + provider_id: str, + tenant_id: str, + authed: bool = True, + authorization_code: Optional[str] = None, + for_list: bool = False, + ): + # Initialize info + self.provider_id = provider_id + self.tenant_id = tenant_id + self.client_type = "streamable" + self.server_url = server_url + + # Authentication info + self.authed = authed + self.authorization_code = authorization_code + if authed: + from core.mcp.auth.auth_provider import OAuthClientProvider + + self.provider = OAuthClientProvider(self.provider_id, self.tenant_id, for_list=for_list) + self.token = self.provider.tokens() + + # Initialize session and client objects + self._session: Optional[ClientSession] = None + self._streams_context: Optional[AbstractContextManager[Any]] = None + self._session_context: Optional[ClientSession] = None + self.exit_stack = ExitStack() + + # Whether the client has been initialized + self._initialized = False + + def __enter__(self): + self._initialize() + self._initialized = True + return self + + def __exit__( + self, exc_type: Optional[type], exc_value: Optional[BaseException], traceback: Optional[TracebackType] + ): + self.cleanup() + + def _initialize( + self, + ): + """Initialize the client with fallback to SSE if streamable connection fails""" + connection_methods: dict[str, Callable[..., AbstractContextManager[Any]]] = { + "mcp": streamablehttp_client, + "sse": sse_client, + } + + parsed_url = urlparse(self.server_url) + path = parsed_url.path + method_name = path.rstrip("/").split("/")[-1] if path else "" + try: + client_factory = connection_methods[method_name] + self.connect_server(client_factory, method_name) + except KeyError: + try: + self.connect_server(sse_client, "sse") + except MCPConnectionError: + self.connect_server(streamablehttp_client, "mcp") + + def connect_server( + self, client_factory: Callable[..., AbstractContextManager[Any]], method_name: str, first_try: bool = True + ): + from core.mcp.auth.auth_flow import auth + + try: + headers = ( + {"Authorization": f"{self.token.token_type.capitalize()} {self.token.access_token}"} + if self.authed and self.token + else {} + ) + self._streams_context = client_factory(url=self.server_url, headers=headers) + if self._streams_context is None: + raise MCPConnectionError("Failed to create connection context") + + # Use exit_stack to manage context managers properly + if method_name == "mcp": + read_stream, write_stream, _ = self.exit_stack.enter_context(self._streams_context) + streams = (read_stream, write_stream) + else: # sse_client + streams = self.exit_stack.enter_context(self._streams_context) + + self._session_context = ClientSession(*streams) + self._session = self.exit_stack.enter_context(self._session_context) + session = cast(ClientSession, self._session) + session.initialize() + return + + except MCPAuthError: + if not self.authed: + raise + try: + auth(self.provider, self.server_url, self.authorization_code) + except Exception as e: + raise ValueError(f"Failed to authenticate: {e}") + self.token = self.provider.tokens() + if first_try: + return self.connect_server(client_factory, method_name, first_try=False) + + except MCPConnectionError: + raise + + def list_tools(self) -> list[Tool]: + """Connect to an MCP server running with SSE transport""" + # List available tools to verify connection + if not self._initialized or not self._session: + raise ValueError("Session not initialized.") + response = self._session.list_tools() + tools = response.tools + return tools + + def invoke_tool(self, tool_name: str, tool_args: dict): + """Call a tool""" + if not self._initialized or not self._session: + raise ValueError("Session not initialized.") + return self._session.call_tool(tool_name, tool_args) + + def cleanup(self): + """Clean up resources""" + try: + # ExitStack will handle proper cleanup of all managed context managers + self.exit_stack.close() + self._session = None + self._session_context = None + self._streams_context = None + self._initialized = False + except Exception as e: + logging.exception("Error during cleanup") + raise ValueError(f"Error during cleanup: {e}") diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py new file mode 100644 index 0000000000..1c2cf570e2 --- /dev/null +++ b/api/core/mcp/server/streamable_http.py @@ -0,0 +1,228 @@ +import json +import logging +from collections.abc import Mapping +from typing import Any, cast + +from configs import dify_config +from controllers.web.passport import generate_session_id +from core.app.app_config.entities import VariableEntity, VariableEntityType +from core.app.entities.app_invoke_entities import InvokeFrom +from core.app.features.rate_limiting.rate_limit import RateLimitGenerator +from core.mcp import types +from core.mcp.types import INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND +from core.mcp.utils import create_mcp_error_response +from core.model_runtime.utils.encoders import jsonable_encoder +from extensions.ext_database import db +from models.model import App, AppMCPServer, AppMode, EndUser +from services.app_generate_service import AppGenerateService + +""" +Apply to MCP HTTP streamable server with stateless http +""" +logger = logging.getLogger(__name__) + + +class MCPServerStreamableHTTPRequestHandler: + def __init__( + self, app: App, request: types.ClientRequest | types.ClientNotification, user_input_form: list[VariableEntity] + ): + self.app = app + self.request = request + mcp_server = db.session.query(AppMCPServer).filter(AppMCPServer.app_id == self.app.id).first() + if not mcp_server: + raise ValueError("MCP server not found") + self.mcp_server: AppMCPServer = mcp_server + self.end_user = self.retrieve_end_user() + self.user_input_form = user_input_form + + @property + def request_type(self): + return type(self.request.root) + + @property + def parameter_schema(self): + parameters, required = self._convert_input_form_to_parameters(self.user_input_form) + if self.app.mode in {AppMode.COMPLETION.value, AppMode.WORKFLOW.value}: + return { + "type": "object", + "properties": parameters, + "required": required, + } + return { + "type": "object", + "properties": { + "query": {"type": "string", "description": "User Input/Question content"}, + **parameters, + }, + "required": ["query", *required], + } + + @property + def capabilities(self): + return types.ServerCapabilities( + tools=types.ToolsCapability(listChanged=False), + ) + + def response(self, response: types.Result | str): + if isinstance(response, str): + sse_content = f"event: ping\ndata: {response}\n\n".encode() + yield sse_content + return + json_response = types.JSONRPCResponse( + jsonrpc="2.0", + id=(self.request.root.model_extra or {}).get("id", 1), + result=response.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + json_data = json.dumps(jsonable_encoder(json_response)) + + sse_content = f"event: message\ndata: {json_data}\n\n".encode() + + yield sse_content + + def error_response(self, code: int, message: str, data=None): + request_id = (self.request.root.model_extra or {}).get("id", 1) or 1 + return create_mcp_error_response(request_id, code, message, data) + + def handle(self): + handle_map = { + types.InitializeRequest: self.initialize, + types.ListToolsRequest: self.list_tools, + types.CallToolRequest: self.invoke_tool, + types.InitializedNotification: self.handle_notification, + types.PingRequest: self.handle_ping, + } + try: + if self.request_type in handle_map: + return self.response(handle_map[self.request_type]()) + else: + return self.error_response(METHOD_NOT_FOUND, f"Method not found: {self.request_type}") + except ValueError as e: + logger.exception("Invalid params") + return self.error_response(INVALID_PARAMS, str(e)) + except Exception as e: + logger.exception("Internal server error") + return self.error_response(INTERNAL_ERROR, f"Internal server error: {str(e)}") + + def handle_notification(self): + return "ping" + + def handle_ping(self): + return types.EmptyResult() + + def initialize(self): + request = cast(types.InitializeRequest, self.request.root) + client_info = request.params.clientInfo + client_name = f"{client_info.name}@{client_info.version}" + if not self.end_user: + end_user = EndUser( + tenant_id=self.app.tenant_id, + app_id=self.app.id, + type="mcp", + name=client_name, + session_id=generate_session_id(), + external_user_id=self.mcp_server.id, + ) + db.session.add(end_user) + db.session.commit() + return types.InitializeResult( + protocolVersion=types.SERVER_LATEST_PROTOCOL_VERSION, + capabilities=self.capabilities, + serverInfo=types.Implementation(name="Dify", version=dify_config.project.version), + instructions=self.mcp_server.description, + ) + + def list_tools(self): + if not self.end_user: + raise ValueError("User not found") + return types.ListToolsResult( + tools=[ + types.Tool( + name=self.app.name, + description=self.mcp_server.description, + inputSchema=self.parameter_schema, + ) + ], + ) + + def invoke_tool(self): + if not self.end_user: + raise ValueError("User not found") + request = cast(types.CallToolRequest, self.request.root) + args = request.params.arguments + if not args: + raise ValueError("No arguments provided") + if self.app.mode in {AppMode.WORKFLOW.value}: + args = {"inputs": args} + elif self.app.mode in {AppMode.COMPLETION.value}: + args = {"query": "", "inputs": args} + else: + args = {"query": args["query"], "inputs": {k: v for k, v in args.items() if k != "query"}} + response = AppGenerateService.generate( + self.app, + self.end_user, + args, + InvokeFrom.SERVICE_API, + streaming=self.app.mode == AppMode.AGENT_CHAT.value, + ) + answer = "" + if isinstance(response, RateLimitGenerator): + for item in response.generator: + data = item + if isinstance(data, str) and data.startswith("data: "): + try: + json_str = data[6:].strip() + parsed_data = json.loads(json_str) + if parsed_data.get("event") == "agent_thought": + answer += parsed_data.get("thought", "") + except json.JSONDecodeError: + continue + if isinstance(response, Mapping): + if self.app.mode in { + AppMode.ADVANCED_CHAT.value, + AppMode.COMPLETION.value, + AppMode.CHAT.value, + AppMode.AGENT_CHAT.value, + }: + answer = response["answer"] + elif self.app.mode in {AppMode.WORKFLOW.value}: + answer = json.dumps(response["data"]["outputs"], ensure_ascii=False) + else: + raise ValueError("Invalid app mode") + # Not support image yet + return types.CallToolResult(content=[types.TextContent(text=answer, type="text")]) + + def retrieve_end_user(self): + return ( + db.session.query(EndUser) + .filter(EndUser.external_user_id == self.mcp_server.id, EndUser.type == "mcp") + .first() + ) + + def _convert_input_form_to_parameters(self, user_input_form: list[VariableEntity]): + parameters: dict[str, dict[str, Any]] = {} + required = [] + for item in user_input_form: + parameters[item.variable] = {} + if item.type in ( + VariableEntityType.FILE, + VariableEntityType.FILE_LIST, + VariableEntityType.EXTERNAL_DATA_TOOL, + ): + continue + if item.required: + required.append(item.variable) + # if the workflow republished, the parameters not changed + # we should not raise error here + try: + description = self.mcp_server.parameters_dict[item.variable] + except KeyError: + description = "" + parameters[item.variable]["description"] = description + if item.type in (VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH): + parameters[item.variable]["type"] = "string" + elif item.type == VariableEntityType.SELECT: + parameters[item.variable]["type"] = "string" + parameters[item.variable]["enum"] = item.options + elif item.type == VariableEntityType.NUMBER: + parameters[item.variable]["type"] = "float" + return parameters, required diff --git a/api/core/mcp/session/base_session.py b/api/core/mcp/session/base_session.py new file mode 100644 index 0000000000..7734b8fdd9 --- /dev/null +++ b/api/core/mcp/session/base_session.py @@ -0,0 +1,415 @@ +import logging +import queue +from collections.abc import Callable +from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError +from contextlib import ExitStack +from datetime import timedelta +from types import TracebackType +from typing import Any, Generic, Self, TypeVar + +from httpx import HTTPStatusError +from pydantic import BaseModel + +from core.mcp.error import MCPAuthError, MCPConnectionError +from core.mcp.types import ( + CancelledNotification, + ClientNotification, + ClientRequest, + ClientResult, + ErrorData, + JSONRPCError, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + MessageMetadata, + RequestId, + RequestParams, + ServerMessageMetadata, + ServerNotification, + ServerRequest, + ServerResult, + SessionMessage, +) + +SendRequestT = TypeVar("SendRequestT", ClientRequest, ServerRequest) +SendResultT = TypeVar("SendResultT", ClientResult, ServerResult) +SendNotificationT = TypeVar("SendNotificationT", ClientNotification, ServerNotification) +ReceiveRequestT = TypeVar("ReceiveRequestT", ClientRequest, ServerRequest) +ReceiveResultT = TypeVar("ReceiveResultT", bound=BaseModel) +ReceiveNotificationT = TypeVar("ReceiveNotificationT", ClientNotification, ServerNotification) +DEFAULT_RESPONSE_READ_TIMEOUT = 1.0 + + +class RequestResponder(Generic[ReceiveRequestT, SendResultT]): + """Handles responding to MCP requests and manages request lifecycle. + + This class MUST be used as a context manager to ensure proper cleanup and + cancellation handling: + + Example: + with request_responder as resp: + resp.respond(result) + + The context manager ensures: + 1. Proper cancellation scope setup and cleanup + 2. Request completion tracking + 3. Cleanup of in-flight requests + """ + + request: ReceiveRequestT + _session: Any + _on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any] + + def __init__( + self, + request_id: RequestId, + request_meta: RequestParams.Meta | None, + request: ReceiveRequestT, + session: """BaseSession[ + SendRequestT, + SendNotificationT, + SendResultT, + ReceiveRequestT, + ReceiveNotificationT + ]""", + on_complete: Callable[["RequestResponder[ReceiveRequestT, SendResultT]"], Any], + ) -> None: + self.request_id = request_id + self.request_meta = request_meta + self.request = request + self._session = session + self._completed = False + self._on_complete = on_complete + self._entered = False # Track if we're in a context manager + + def __enter__(self) -> "RequestResponder[ReceiveRequestT, SendResultT]": + """Enter the context manager, enabling request cancellation tracking.""" + self._entered = True + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the context manager, performing cleanup and notifying completion.""" + try: + if self._completed: + self._on_complete(self) + finally: + self._entered = False + + def respond(self, response: SendResultT | ErrorData) -> None: + """Send a response for this request. + + Must be called within a context manager block. + Raises: + RuntimeError: If not used within a context manager + AssertionError: If request was already responded to + """ + if not self._entered: + raise RuntimeError("RequestResponder must be used as a context manager") + assert not self._completed, "Request already responded to" + + self._completed = True + + self._session._send_response(request_id=self.request_id, response=response) + + def cancel(self) -> None: + """Cancel this request and mark it as completed.""" + if not self._entered: + raise RuntimeError("RequestResponder must be used as a context manager") + + self._completed = True # Mark as completed so it's removed from in_flight + # Send an error response to indicate cancellation + self._session._send_response( + request_id=self.request_id, + response=ErrorData(code=0, message="Request cancelled", data=None), + ) + + +class BaseSession( + Generic[ + SendRequestT, + SendNotificationT, + SendResultT, + ReceiveRequestT, + ReceiveNotificationT, + ], +): + """ + Implements an MCP "session" on top of read/write streams, including features + like request/response linking, notifications, and progress. + + This class is a context manager that automatically starts processing + messages when entered. + """ + + _response_streams: dict[RequestId, queue.Queue[JSONRPCResponse | JSONRPCError]] + _request_id: int + _in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]] + _receive_request_type: type[ReceiveRequestT] + _receive_notification_type: type[ReceiveNotificationT] + + def __init__( + self, + read_stream: queue.Queue, + write_stream: queue.Queue, + receive_request_type: type[ReceiveRequestT], + receive_notification_type: type[ReceiveNotificationT], + # If none, reading will never time out + read_timeout_seconds: timedelta | None = None, + ) -> None: + self._read_stream = read_stream + self._write_stream = write_stream + self._response_streams = {} + self._request_id = 0 + self._receive_request_type = receive_request_type + self._receive_notification_type = receive_notification_type + self._session_read_timeout_seconds = read_timeout_seconds + self._in_flight = {} + self._exit_stack = ExitStack() + # Initialize executor and future to None for proper cleanup checks + self._executor: ThreadPoolExecutor | None = None + self._receiver_future: Future | None = None + + def __enter__(self) -> Self: + # The thread pool is dedicated to running `_receive_loop`. Setting `max_workers` to 1 + # ensures no unnecessary threads are created. + self._executor = ThreadPoolExecutor(max_workers=1) + self._receiver_future = self._executor.submit(self._receive_loop) + return self + + def check_receiver_status(self) -> None: + """`check_receiver_status` ensures that any exceptions raised during the + execution of `_receive_loop` are retrieved and propagated.""" + if self._receiver_future and self._receiver_future.done(): + self._receiver_future.result() + + def __exit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None + ) -> None: + self._read_stream.put(None) + self._write_stream.put(None) + + # Wait for the receiver loop to finish + if self._receiver_future: + try: + self._receiver_future.result(timeout=5.0) # Wait up to 5 seconds + except TimeoutError: + # If the receiver loop is still running after timeout, we'll force shutdown + pass + + # Shutdown the executor + if self._executor: + self._executor.shutdown(wait=True) + + def send_request( + self, + request: SendRequestT, + result_type: type[ReceiveResultT], + request_read_timeout_seconds: timedelta | None = None, + metadata: MessageMetadata = None, + ) -> ReceiveResultT: + """ + Sends a request and wait for a response. Raises an McpError if the + response contains an error. If a request read timeout is provided, it + will take precedence over the session read timeout. + + Do not use this method to emit notifications! Use send_notification() + instead. + """ + self.check_receiver_status() + + request_id = self._request_id + self._request_id = request_id + 1 + + response_queue: queue.Queue[JSONRPCResponse | JSONRPCError] = queue.Queue() + self._response_streams[request_id] = response_queue + + try: + jsonrpc_request = JSONRPCRequest( + jsonrpc="2.0", + id=request_id, + **request.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + + self._write_stream.put(SessionMessage(message=JSONRPCMessage(jsonrpc_request), metadata=metadata)) + timeout = DEFAULT_RESPONSE_READ_TIMEOUT + if request_read_timeout_seconds is not None: + timeout = float(request_read_timeout_seconds.total_seconds()) + elif self._session_read_timeout_seconds is not None: + timeout = float(self._session_read_timeout_seconds.total_seconds()) + while True: + try: + response_or_error = response_queue.get(timeout=timeout) + break + except queue.Empty: + self.check_receiver_status() + continue + + if response_or_error is None: + raise MCPConnectionError( + ErrorData( + code=500, + message="No response received", + ) + ) + elif isinstance(response_or_error, JSONRPCError): + if response_or_error.error.code == 401: + raise MCPAuthError( + ErrorData(code=response_or_error.error.code, message=response_or_error.error.message) + ) + else: + raise MCPConnectionError( + ErrorData(code=response_or_error.error.code, message=response_or_error.error.message) + ) + else: + return result_type.model_validate(response_or_error.result) + + finally: + self._response_streams.pop(request_id, None) + + def send_notification( + self, + notification: SendNotificationT, + related_request_id: RequestId | None = None, + ) -> None: + """ + Emits a notification, which is a one-way message that does not expect + a response. + """ + self.check_receiver_status() + + # Some transport implementations may need to set the related_request_id + # to attribute to the notifications to the request that triggered them. + jsonrpc_notification = JSONRPCNotification( + jsonrpc="2.0", + **notification.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + session_message = SessionMessage( + message=JSONRPCMessage(jsonrpc_notification), + metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None, + ) + self._write_stream.put(session_message) + + def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None: + if isinstance(response, ErrorData): + jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response) + session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_error)) + self._write_stream.put(session_message) + else: + jsonrpc_response = JSONRPCResponse( + jsonrpc="2.0", + id=request_id, + result=response.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_response)) + self._write_stream.put(session_message) + + def _receive_loop(self) -> None: + """ + Main message processing loop. + In a real synchronous implementation, this would likely run in a separate thread. + """ + while True: + try: + # Attempt to receive a message (this would be blocking in a synchronous context) + message = self._read_stream.get(timeout=DEFAULT_RESPONSE_READ_TIMEOUT) + if message is None: + break + if isinstance(message, HTTPStatusError): + response_queue = self._response_streams.get(self._request_id - 1) + if response_queue is not None: + response_queue.put( + JSONRPCError( + jsonrpc="2.0", + id=self._request_id - 1, + error=ErrorData(code=message.response.status_code, message=message.args[0]), + ) + ) + else: + self._handle_incoming(RuntimeError(f"Received response with an unknown request ID: {message}")) + elif isinstance(message, Exception): + self._handle_incoming(message) + elif isinstance(message.message.root, JSONRPCRequest): + validated_request = self._receive_request_type.model_validate( + message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + + responder = RequestResponder( + request_id=message.message.root.id, + request_meta=validated_request.root.params.meta if validated_request.root.params else None, + request=validated_request, + session=self, + on_complete=lambda r: self._in_flight.pop(r.request_id, None), + ) + + self._in_flight[responder.request_id] = responder + self._received_request(responder) + + if not responder._completed: + self._handle_incoming(responder) + + elif isinstance(message.message.root, JSONRPCNotification): + try: + notification = self._receive_notification_type.model_validate( + message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + # Handle cancellation notifications + if isinstance(notification.root, CancelledNotification): + cancelled_id = notification.root.params.requestId + if cancelled_id in self._in_flight: + self._in_flight[cancelled_id].cancel() + else: + self._received_notification(notification) + self._handle_incoming(notification) + except Exception as e: + # For other validation errors, log and continue + logging.warning(f"Failed to validate notification: {e}. Message was: {message.message.root}") + else: # Response or error + response_queue = self._response_streams.get(message.message.root.id) + if response_queue is not None: + response_queue.put(message.message.root) + else: + self._handle_incoming(RuntimeError(f"Server Error: {message}")) + except queue.Empty: + continue + except Exception as e: + logging.exception("Error in message processing loop") + raise + + def _received_request(self, responder: RequestResponder[ReceiveRequestT, SendResultT]) -> None: + """ + Can be overridden by subclasses to handle a request without needing to + listen on the message stream. + + If the request is responded to within this method, it will not be + forwarded on to the message stream. + """ + pass + + def _received_notification(self, notification: ReceiveNotificationT) -> None: + """ + Can be overridden by subclasses to handle a notification without needing + to listen on the message stream. + """ + pass + + def send_progress_notification( + self, progress_token: str | int, progress: float, total: float | None = None + ) -> None: + """ + Sends a progress notification for a request that is currently being + processed. + """ + pass + + def _handle_incoming( + self, + req: RequestResponder[ReceiveRequestT, SendResultT] | ReceiveNotificationT | Exception, + ) -> None: + """A generic handler for incoming messages. Overwritten by subclasses.""" + pass diff --git a/api/core/mcp/session/client_session.py b/api/core/mcp/session/client_session.py new file mode 100644 index 0000000000..ed2ad508ab --- /dev/null +++ b/api/core/mcp/session/client_session.py @@ -0,0 +1,365 @@ +from datetime import timedelta +from typing import Any, Protocol + +from pydantic import AnyUrl, TypeAdapter + +from configs import dify_config +from core.mcp import types +from core.mcp.entities import SUPPORTED_PROTOCOL_VERSIONS, RequestContext +from core.mcp.session.base_session import BaseSession, RequestResponder + +DEFAULT_CLIENT_INFO = types.Implementation(name="Dify", version=dify_config.project.version) + + +class SamplingFnT(Protocol): + def __call__( + self, + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, + ) -> types.CreateMessageResult | types.ErrorData: ... + + +class ListRootsFnT(Protocol): + def __call__(self, context: RequestContext["ClientSession", Any]) -> types.ListRootsResult | types.ErrorData: ... + + +class LoggingFnT(Protocol): + def __call__( + self, + params: types.LoggingMessageNotificationParams, + ) -> None: ... + + +class MessageHandlerFnT(Protocol): + def __call__( + self, + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: ... + + +def _default_message_handler( + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, +) -> None: + if isinstance(message, Exception): + raise ValueError(str(message)) + elif isinstance(message, (types.ServerNotification | RequestResponder)): + pass + + +def _default_sampling_callback( + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, +) -> types.CreateMessageResult | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="Sampling not supported", + ) + + +def _default_list_roots_callback( + context: RequestContext["ClientSession", Any], +) -> types.ListRootsResult | types.ErrorData: + return types.ErrorData( + code=types.INVALID_REQUEST, + message="List roots not supported", + ) + + +def _default_logging_callback( + params: types.LoggingMessageNotificationParams, +) -> None: + pass + + +ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData) + + +class ClientSession( + BaseSession[ + types.ClientRequest, + types.ClientNotification, + types.ClientResult, + types.ServerRequest, + types.ServerNotification, + ] +): + def __init__( + self, + read_stream, + write_stream, + read_timeout_seconds: timedelta | None = None, + sampling_callback: SamplingFnT | None = None, + list_roots_callback: ListRootsFnT | None = None, + logging_callback: LoggingFnT | None = None, + message_handler: MessageHandlerFnT | None = None, + client_info: types.Implementation | None = None, + ) -> None: + super().__init__( + read_stream, + write_stream, + types.ServerRequest, + types.ServerNotification, + read_timeout_seconds=read_timeout_seconds, + ) + self._client_info = client_info or DEFAULT_CLIENT_INFO + self._sampling_callback = sampling_callback or _default_sampling_callback + self._list_roots_callback = list_roots_callback or _default_list_roots_callback + self._logging_callback = logging_callback or _default_logging_callback + self._message_handler = message_handler or _default_message_handler + + def initialize(self) -> types.InitializeResult: + sampling = types.SamplingCapability() + roots = types.RootsCapability( + # TODO: Should this be based on whether we + # _will_ send notifications, or only whether + # they're supported? + listChanged=True, + ) + + result = self.send_request( + types.ClientRequest( + types.InitializeRequest( + method="initialize", + params=types.InitializeRequestParams( + protocolVersion=types.LATEST_PROTOCOL_VERSION, + capabilities=types.ClientCapabilities( + sampling=sampling, + experimental=None, + roots=roots, + ), + clientInfo=self._client_info, + ), + ) + ), + types.InitializeResult, + ) + + if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS: + raise RuntimeError(f"Unsupported protocol version from the server: {result.protocolVersion}") + + self.send_notification( + types.ClientNotification(types.InitializedNotification(method="notifications/initialized")) + ) + + return result + + def send_ping(self) -> types.EmptyResult: + """Send a ping request.""" + return self.send_request( + types.ClientRequest( + types.PingRequest( + method="ping", + ) + ), + types.EmptyResult, + ) + + def send_progress_notification( + self, progress_token: str | int, progress: float, total: float | None = None + ) -> None: + """Send a progress notification.""" + self.send_notification( + types.ClientNotification( + types.ProgressNotification( + method="notifications/progress", + params=types.ProgressNotificationParams( + progressToken=progress_token, + progress=progress, + total=total, + ), + ), + ) + ) + + def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult: + """Send a logging/setLevel request.""" + return self.send_request( + types.ClientRequest( + types.SetLevelRequest( + method="logging/setLevel", + params=types.SetLevelRequestParams(level=level), + ) + ), + types.EmptyResult, + ) + + def list_resources(self) -> types.ListResourcesResult: + """Send a resources/list request.""" + return self.send_request( + types.ClientRequest( + types.ListResourcesRequest( + method="resources/list", + ) + ), + types.ListResourcesResult, + ) + + def list_resource_templates(self) -> types.ListResourceTemplatesResult: + """Send a resources/templates/list request.""" + return self.send_request( + types.ClientRequest( + types.ListResourceTemplatesRequest( + method="resources/templates/list", + ) + ), + types.ListResourceTemplatesResult, + ) + + def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult: + """Send a resources/read request.""" + return self.send_request( + types.ClientRequest( + types.ReadResourceRequest( + method="resources/read", + params=types.ReadResourceRequestParams(uri=uri), + ) + ), + types.ReadResourceResult, + ) + + def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: + """Send a resources/subscribe request.""" + return self.send_request( + types.ClientRequest( + types.SubscribeRequest( + method="resources/subscribe", + params=types.SubscribeRequestParams(uri=uri), + ) + ), + types.EmptyResult, + ) + + def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: + """Send a resources/unsubscribe request.""" + return self.send_request( + types.ClientRequest( + types.UnsubscribeRequest( + method="resources/unsubscribe", + params=types.UnsubscribeRequestParams(uri=uri), + ) + ), + types.EmptyResult, + ) + + def call_tool( + self, + name: str, + arguments: dict[str, Any] | None = None, + read_timeout_seconds: timedelta | None = None, + ) -> types.CallToolResult: + """Send a tools/call request.""" + + return self.send_request( + types.ClientRequest( + types.CallToolRequest( + method="tools/call", + params=types.CallToolRequestParams(name=name, arguments=arguments), + ) + ), + types.CallToolResult, + request_read_timeout_seconds=read_timeout_seconds, + ) + + def list_prompts(self) -> types.ListPromptsResult: + """Send a prompts/list request.""" + return self.send_request( + types.ClientRequest( + types.ListPromptsRequest( + method="prompts/list", + ) + ), + types.ListPromptsResult, + ) + + def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult: + """Send a prompts/get request.""" + return self.send_request( + types.ClientRequest( + types.GetPromptRequest( + method="prompts/get", + params=types.GetPromptRequestParams(name=name, arguments=arguments), + ) + ), + types.GetPromptResult, + ) + + def complete( + self, + ref: types.ResourceReference | types.PromptReference, + argument: dict[str, str], + ) -> types.CompleteResult: + """Send a completion/complete request.""" + return self.send_request( + types.ClientRequest( + types.CompleteRequest( + method="completion/complete", + params=types.CompleteRequestParams( + ref=ref, + argument=types.CompletionArgument(**argument), + ), + ) + ), + types.CompleteResult, + ) + + def list_tools(self) -> types.ListToolsResult: + """Send a tools/list request.""" + return self.send_request( + types.ClientRequest( + types.ListToolsRequest( + method="tools/list", + ) + ), + types.ListToolsResult, + ) + + def send_roots_list_changed(self) -> None: + """Send a roots/list_changed notification.""" + self.send_notification( + types.ClientNotification( + types.RootsListChangedNotification( + method="notifications/roots/list_changed", + ) + ) + ) + + def _received_request(self, responder: RequestResponder[types.ServerRequest, types.ClientResult]) -> None: + ctx = RequestContext[ClientSession, Any]( + request_id=responder.request_id, + meta=responder.request_meta, + session=self, + lifespan_context=None, + ) + + match responder.request.root: + case types.CreateMessageRequest(params=params): + with responder: + response = self._sampling_callback(ctx, params) + client_response = ClientResponse.validate_python(response) + responder.respond(client_response) + + case types.ListRootsRequest(): + with responder: + list_roots_response = self._list_roots_callback(ctx) + client_response = ClientResponse.validate_python(list_roots_response) + responder.respond(client_response) + + case types.PingRequest(): + with responder: + return responder.respond(types.ClientResult(root=types.EmptyResult())) + + def _handle_incoming( + self, + req: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: + """Handle incoming messages by forwarding to the message handler.""" + self._message_handler(req) + + def _received_notification(self, notification: types.ServerNotification) -> None: + """Handle notifications from the server.""" + # Process specific notification types + match notification.root: + case types.LoggingMessageNotification(params=params): + self._logging_callback(params) + case _: + pass diff --git a/api/core/mcp/types.py b/api/core/mcp/types.py new file mode 100644 index 0000000000..99d985a781 --- /dev/null +++ b/api/core/mcp/types.py @@ -0,0 +1,1217 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import ( + Annotated, + Any, + Generic, + Literal, + Optional, + TypeAlias, + TypeVar, +) + +from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel +from pydantic.networks import AnyUrl, UrlConstraints + +""" +Model Context Protocol bindings for Python + +These bindings were generated from https://github.com/modelcontextprotocol/specification, +using Claude, with a prompt something like the following: + +Generate idiomatic Python bindings for this schema for MCP, or the "Model Context +Protocol." The schema is defined in TypeScript, but there's also a JSON Schema version +for reference. + +* For the bindings, let's use Pydantic V2 models. +* Each model should allow extra fields everywhere, by specifying `model_config = + ConfigDict(extra='allow')`. Do this in every case, instead of a custom base class. +* Union types should be represented with a Pydantic `RootModel`. +* Define additional model classes instead of using dictionaries. Do this even if they're + not separate types in the schema. +""" +# Client support both version, not support 2025-06-18 yet. +LATEST_PROTOCOL_VERSION = "2025-03-26" +# Server support 2024-11-05 to allow claude to use. +SERVER_LATEST_PROTOCOL_VERSION = "2024-11-05" +ProgressToken = str | int +Cursor = str +Role = Literal["user", "assistant"] +RequestId = Annotated[int | str, Field(union_mode="left_to_right")] +AnyFunction: TypeAlias = Callable[..., Any] + + +class RequestParams(BaseModel): + class Meta(BaseModel): + progressToken: ProgressToken | None = None + """ + If specified, the caller requests out-of-band progress notifications for + this request (as represented by notifications/progress). The value of this + parameter is an opaque token that will be attached to any subsequent + notifications. The receiver is not obligated to provide these notifications. + """ + + model_config = ConfigDict(extra="allow") + + meta: Meta | None = Field(alias="_meta", default=None) + + +class NotificationParams(BaseModel): + class Meta(BaseModel): + model_config = ConfigDict(extra="allow") + + meta: Meta | None = Field(alias="_meta", default=None) + """ + This parameter name is reserved by MCP to allow clients and servers to attach + additional metadata to their notifications. + """ + + +RequestParamsT = TypeVar("RequestParamsT", bound=RequestParams | dict[str, Any] | None) +NotificationParamsT = TypeVar("NotificationParamsT", bound=NotificationParams | dict[str, Any] | None) +MethodT = TypeVar("MethodT", bound=str) + + +class Request(BaseModel, Generic[RequestParamsT, MethodT]): + """Base class for JSON-RPC requests.""" + + method: MethodT + params: RequestParamsT + model_config = ConfigDict(extra="allow") + + +class PaginatedRequest(Request[RequestParamsT, MethodT]): + cursor: Cursor | None = None + """ + An opaque token representing the current pagination position. + If provided, the server should return results starting after this cursor. + """ + + +class Notification(BaseModel, Generic[NotificationParamsT, MethodT]): + """Base class for JSON-RPC notifications.""" + + method: MethodT + params: NotificationParamsT + model_config = ConfigDict(extra="allow") + + +class Result(BaseModel): + """Base class for JSON-RPC results.""" + + model_config = ConfigDict(extra="allow") + + meta: dict[str, Any] | None = Field(alias="_meta", default=None) + """ + This result property is reserved by the protocol to allow clients and servers to + attach additional metadata to their responses. + """ + + +class PaginatedResult(Result): + nextCursor: Cursor | None = None + """ + An opaque token representing the pagination position after the last returned result. + If present, there may be more results available. + """ + + +class JSONRPCRequest(Request[dict[str, Any] | None, str]): + """A request that expects a response.""" + + jsonrpc: Literal["2.0"] + id: RequestId + method: str + params: dict[str, Any] | None = None + + +class JSONRPCNotification(Notification[dict[str, Any] | None, str]): + """A notification which does not expect a response.""" + + jsonrpc: Literal["2.0"] + params: dict[str, Any] | None = None + + +class JSONRPCResponse(BaseModel): + """A successful (non-error) response to a request.""" + + jsonrpc: Literal["2.0"] + id: RequestId + result: dict[str, Any] + model_config = ConfigDict(extra="allow") + + +# Standard JSON-RPC error codes +PARSE_ERROR = -32700 +INVALID_REQUEST = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_PARAMS = -32602 +INTERNAL_ERROR = -32603 + + +class ErrorData(BaseModel): + """Error information for JSON-RPC error responses.""" + + code: int + """The error type that occurred.""" + + message: str + """ + A short description of the error. The message SHOULD be limited to a concise single + sentence. + """ + + data: Any | None = None + """ + Additional information about the error. The value of this member is defined by the + sender (e.g. detailed error information, nested errors etc.). + """ + + model_config = ConfigDict(extra="allow") + + +class JSONRPCError(BaseModel): + """A response to a request that indicates an error occurred.""" + + jsonrpc: Literal["2.0"] + id: str | int + error: ErrorData + model_config = ConfigDict(extra="allow") + + +class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError]): + pass + + +class EmptyResult(Result): + """A response that indicates success but carries no data.""" + + +class Implementation(BaseModel): + """Describes the name and version of an MCP implementation.""" + + name: str + version: str + model_config = ConfigDict(extra="allow") + + +class RootsCapability(BaseModel): + """Capability for root operations.""" + + listChanged: bool | None = None + """Whether the client supports notifications for changes to the roots list.""" + model_config = ConfigDict(extra="allow") + + +class SamplingCapability(BaseModel): + """Capability for logging operations.""" + + model_config = ConfigDict(extra="allow") + + +class ClientCapabilities(BaseModel): + """Capabilities a client may support.""" + + experimental: dict[str, dict[str, Any]] | None = None + """Experimental, non-standard capabilities that the client supports.""" + sampling: SamplingCapability | None = None + """Present if the client supports sampling from an LLM.""" + roots: RootsCapability | None = None + """Present if the client supports listing roots.""" + model_config = ConfigDict(extra="allow") + + +class PromptsCapability(BaseModel): + """Capability for prompts operations.""" + + listChanged: bool | None = None + """Whether this server supports notifications for changes to the prompt list.""" + model_config = ConfigDict(extra="allow") + + +class ResourcesCapability(BaseModel): + """Capability for resources operations.""" + + subscribe: bool | None = None + """Whether this server supports subscribing to resource updates.""" + listChanged: bool | None = None + """Whether this server supports notifications for changes to the resource list.""" + model_config = ConfigDict(extra="allow") + + +class ToolsCapability(BaseModel): + """Capability for tools operations.""" + + listChanged: bool | None = None + """Whether this server supports notifications for changes to the tool list.""" + model_config = ConfigDict(extra="allow") + + +class LoggingCapability(BaseModel): + """Capability for logging operations.""" + + model_config = ConfigDict(extra="allow") + + +class ServerCapabilities(BaseModel): + """Capabilities that a server may support.""" + + experimental: dict[str, dict[str, Any]] | None = None + """Experimental, non-standard capabilities that the server supports.""" + logging: LoggingCapability | None = None + """Present if the server supports sending log messages to the client.""" + prompts: PromptsCapability | None = None + """Present if the server offers any prompt templates.""" + resources: ResourcesCapability | None = None + """Present if the server offers any resources to read.""" + tools: ToolsCapability | None = None + """Present if the server offers any tools to call.""" + model_config = ConfigDict(extra="allow") + + +class InitializeRequestParams(RequestParams): + """Parameters for the initialize request.""" + + protocolVersion: str | int + """The latest version of the Model Context Protocol that the client supports.""" + capabilities: ClientCapabilities + clientInfo: Implementation + model_config = ConfigDict(extra="allow") + + +class InitializeRequest(Request[InitializeRequestParams, Literal["initialize"]]): + """ + This request is sent from the client to the server when it first connects, asking it + to begin initialization. + """ + + method: Literal["initialize"] + params: InitializeRequestParams + + +class InitializeResult(Result): + """After receiving an initialize request from the client, the server sends this.""" + + protocolVersion: str | int + """The version of the Model Context Protocol that the server wants to use.""" + capabilities: ServerCapabilities + serverInfo: Implementation + instructions: str | None = None + """Instructions describing how to use the server and its features.""" + + +class InitializedNotification(Notification[NotificationParams | None, Literal["notifications/initialized"]]): + """ + This notification is sent from the client to the server after initialization has + finished. + """ + + method: Literal["notifications/initialized"] + params: NotificationParams | None = None + + +class PingRequest(Request[RequestParams | None, Literal["ping"]]): + """ + A ping, issued by either the server or the client, to check that the other party is + still alive. + """ + + method: Literal["ping"] + params: RequestParams | None = None + + +class ProgressNotificationParams(NotificationParams): + """Parameters for progress notifications.""" + + progressToken: ProgressToken + """ + The progress token which was given in the initial request, used to associate this + notification with the request that is proceeding. + """ + progress: float + """ + The progress thus far. This should increase every time progress is made, even if the + total is unknown. + """ + total: float | None = None + """Total number of items to process (or total progress required), if known.""" + model_config = ConfigDict(extra="allow") + + +class ProgressNotification(Notification[ProgressNotificationParams, Literal["notifications/progress"]]): + """ + An out-of-band notification used to inform the receiver of a progress update for a + long-running request. + """ + + method: Literal["notifications/progress"] + params: ProgressNotificationParams + + +class ListResourcesRequest(PaginatedRequest[RequestParams | None, Literal["resources/list"]]): + """Sent from the client to request a list of resources the server has.""" + + method: Literal["resources/list"] + params: RequestParams | None = None + + +class Annotations(BaseModel): + audience: list[Role] | None = None + priority: Annotated[float, Field(ge=0.0, le=1.0)] | None = None + model_config = ConfigDict(extra="allow") + + +class Resource(BaseModel): + """A known resource that the server is capable of reading.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of this resource.""" + name: str + """A human-readable name for this resource.""" + description: str | None = None + """A description of what this resource represents.""" + mimeType: str | None = None + """The MIME type of this resource, if known.""" + size: int | None = None + """ + The size of the raw resource content, in bytes (i.e., before base64 encoding + or any tokenization), if known. + + This can be used by Hosts to display file sizes and estimate context window usage. + """ + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + +class ResourceTemplate(BaseModel): + """A template description for resources available on the server.""" + + uriTemplate: str + """ + A URI template (according to RFC 6570) that can be used to construct resource + URIs. + """ + name: str + """A human-readable name for the type of resource this template refers to.""" + description: str | None = None + """A human-readable description of what this template is for.""" + mimeType: str | None = None + """ + The MIME type for all resources that match this template. This should only be + included if all resources matching this template have the same type. + """ + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + +class ListResourcesResult(PaginatedResult): + """The server's response to a resources/list request from the client.""" + + resources: list[Resource] + + +class ListResourceTemplatesRequest(PaginatedRequest[RequestParams | None, Literal["resources/templates/list"]]): + """Sent from the client to request a list of resource templates the server has.""" + + method: Literal["resources/templates/list"] + params: RequestParams | None = None + + +class ListResourceTemplatesResult(PaginatedResult): + """The server's response to a resources/templates/list request from the client.""" + + resourceTemplates: list[ResourceTemplate] + + +class ReadResourceRequestParams(RequestParams): + """Parameters for reading a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource to read. The URI can use any protocol; it is up to the + server how to interpret it. + """ + model_config = ConfigDict(extra="allow") + + +class ReadResourceRequest(Request[ReadResourceRequestParams, Literal["resources/read"]]): + """Sent from the client to the server, to read a specific resource URI.""" + + method: Literal["resources/read"] + params: ReadResourceRequestParams + + +class ResourceContents(BaseModel): + """The contents of a specific resource or sub-resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of this resource.""" + mimeType: str | None = None + """The MIME type of this resource, if known.""" + model_config = ConfigDict(extra="allow") + + +class TextResourceContents(ResourceContents): + """Text contents of a resource.""" + + text: str + """ + The text of the item. This must only be set if the item can actually be represented + as text (not binary data). + """ + + +class BlobResourceContents(ResourceContents): + """Binary contents of a resource.""" + + blob: str + """A base64-encoded string representing the binary data of the item.""" + + +class ReadResourceResult(Result): + """The server's response to a resources/read request from the client.""" + + contents: list[TextResourceContents | BlobResourceContents] + + +class ResourceListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/resources/list_changed"]] +): + """ + An optional notification from the server to the client, informing it that the list + of resources it can read from has changed. + """ + + method: Literal["notifications/resources/list_changed"] + params: NotificationParams | None = None + + +class SubscribeRequestParams(RequestParams): + """Parameters for subscribing to a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource to subscribe to. The URI can use any protocol; it is up to + the server how to interpret it. + """ + model_config = ConfigDict(extra="allow") + + +class SubscribeRequest(Request[SubscribeRequestParams, Literal["resources/subscribe"]]): + """ + Sent from the client to request resources/updated notifications from the server + whenever a particular resource changes. + """ + + method: Literal["resources/subscribe"] + params: SubscribeRequestParams + + +class UnsubscribeRequestParams(RequestParams): + """Parameters for unsubscribing from a resource.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """The URI of the resource to unsubscribe from.""" + model_config = ConfigDict(extra="allow") + + +class UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal["resources/unsubscribe"]]): + """ + Sent from the client to request cancellation of resources/updated notifications from + the server. + """ + + method: Literal["resources/unsubscribe"] + params: UnsubscribeRequestParams + + +class ResourceUpdatedNotificationParams(NotificationParams): + """Parameters for resource update notifications.""" + + uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + """ + The URI of the resource that has been updated. This might be a sub-resource of the + one that the client actually subscribed to. + """ + model_config = ConfigDict(extra="allow") + + +class ResourceUpdatedNotification( + Notification[ResourceUpdatedNotificationParams, Literal["notifications/resources/updated"]] +): + """ + A notification from the server to the client, informing it that a resource has + changed and may need to be read again. + """ + + method: Literal["notifications/resources/updated"] + params: ResourceUpdatedNotificationParams + + +class ListPromptsRequest(PaginatedRequest[RequestParams | None, Literal["prompts/list"]]): + """Sent from the client to request a list of prompts and prompt templates.""" + + method: Literal["prompts/list"] + params: RequestParams | None = None + + +class PromptArgument(BaseModel): + """An argument for a prompt template.""" + + name: str + """The name of the argument.""" + description: str | None = None + """A human-readable description of the argument.""" + required: bool | None = None + """Whether this argument must be provided.""" + model_config = ConfigDict(extra="allow") + + +class Prompt(BaseModel): + """A prompt or prompt template that the server offers.""" + + name: str + """The name of the prompt or prompt template.""" + description: str | None = None + """An optional description of what this prompt provides.""" + arguments: list[PromptArgument] | None = None + """A list of arguments to use for templating the prompt.""" + model_config = ConfigDict(extra="allow") + + +class ListPromptsResult(PaginatedResult): + """The server's response to a prompts/list request from the client.""" + + prompts: list[Prompt] + + +class GetPromptRequestParams(RequestParams): + """Parameters for getting a prompt.""" + + name: str + """The name of the prompt or prompt template.""" + arguments: dict[str, str] | None = None + """Arguments to use for templating the prompt.""" + model_config = ConfigDict(extra="allow") + + +class GetPromptRequest(Request[GetPromptRequestParams, Literal["prompts/get"]]): + """Used by the client to get a prompt provided by the server.""" + + method: Literal["prompts/get"] + params: GetPromptRequestParams + + +class TextContent(BaseModel): + """Text content for a message.""" + + type: Literal["text"] + text: str + """The text content of the message.""" + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + +class ImageContent(BaseModel): + """Image content for a message.""" + + type: Literal["image"] + data: str + """The base64-encoded image data.""" + mimeType: str + """ + The MIME type of the image. Different providers may support different + image types. + """ + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + +class SamplingMessage(BaseModel): + """Describes a message issued to or received from an LLM API.""" + + role: Role + content: TextContent | ImageContent + model_config = ConfigDict(extra="allow") + + +class EmbeddedResource(BaseModel): + """ + The contents of a resource, embedded into a prompt or tool call result. + + It is up to the client how best to render embedded resources for the benefit + of the LLM and/or the user. + """ + + type: Literal["resource"] + resource: TextResourceContents | BlobResourceContents + annotations: Annotations | None = None + model_config = ConfigDict(extra="allow") + + +class PromptMessage(BaseModel): + """Describes a message returned as part of a prompt.""" + + role: Role + content: TextContent | ImageContent | EmbeddedResource + model_config = ConfigDict(extra="allow") + + +class GetPromptResult(Result): + """The server's response to a prompts/get request from the client.""" + + description: str | None = None + """An optional description for the prompt.""" + messages: list[PromptMessage] + + +class PromptListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/prompts/list_changed"]] +): + """ + An optional notification from the server to the client, informing it that the list + of prompts it offers has changed. + """ + + method: Literal["notifications/prompts/list_changed"] + params: NotificationParams | None = None + + +class ListToolsRequest(PaginatedRequest[RequestParams | None, Literal["tools/list"]]): + """Sent from the client to request a list of tools the server has.""" + + method: Literal["tools/list"] + params: RequestParams | None = None + + +class ToolAnnotations(BaseModel): + """ + Additional properties describing a Tool to clients. + + NOTE: all properties in ToolAnnotations are **hints**. + They are not guaranteed to provide a faithful description of + tool behavior (including descriptive properties like `title`). + + Clients should never make tool use decisions based on ToolAnnotations + received from untrusted servers. + """ + + title: str | None = None + """A human-readable title for the tool.""" + + readOnlyHint: bool | None = None + """ + If true, the tool does not modify its environment. + Default: false + """ + + destructiveHint: bool | None = None + """ + If true, the tool may perform destructive updates to its environment. + If false, the tool performs only additive updates. + (This property is meaningful only when `readOnlyHint == false`) + Default: true + """ + + idempotentHint: bool | None = None + """ + If true, calling the tool repeatedly with the same arguments + will have no additional effect on the its environment. + (This property is meaningful only when `readOnlyHint == false`) + Default: false + """ + + openWorldHint: bool | None = None + """ + If true, this tool may interact with an "open world" of external + entities. If false, the tool's domain of interaction is closed. + For example, the world of a web search tool is open, whereas that + of a memory tool is not. + Default: true + """ + model_config = ConfigDict(extra="allow") + + +class Tool(BaseModel): + """Definition for a tool the client can call.""" + + name: str + """The name of the tool.""" + description: str | None = None + """A human-readable description of the tool.""" + inputSchema: dict[str, Any] + """A JSON Schema object defining the expected parameters for the tool.""" + annotations: ToolAnnotations | None = None + """Optional additional tool information.""" + model_config = ConfigDict(extra="allow") + + +class ListToolsResult(PaginatedResult): + """The server's response to a tools/list request from the client.""" + + tools: list[Tool] + + +class CallToolRequestParams(RequestParams): + """Parameters for calling a tool.""" + + name: str + arguments: dict[str, Any] | None = None + model_config = ConfigDict(extra="allow") + + +class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]): + """Used by the client to invoke a tool provided by the server.""" + + method: Literal["tools/call"] + params: CallToolRequestParams + + +class CallToolResult(Result): + """The server's response to a tool call.""" + + content: list[TextContent | ImageContent | EmbeddedResource] + isError: bool = False + + +class ToolListChangedNotification(Notification[NotificationParams | None, Literal["notifications/tools/list_changed"]]): + """ + An optional notification from the server to the client, informing it that the list + of tools it offers has changed. + """ + + method: Literal["notifications/tools/list_changed"] + params: NotificationParams | None = None + + +LoggingLevel = Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] + + +class SetLevelRequestParams(RequestParams): + """Parameters for setting the logging level.""" + + level: LoggingLevel + """The level of logging that the client wants to receive from the server.""" + model_config = ConfigDict(extra="allow") + + +class SetLevelRequest(Request[SetLevelRequestParams, Literal["logging/setLevel"]]): + """A request from the client to the server, to enable or adjust logging.""" + + method: Literal["logging/setLevel"] + params: SetLevelRequestParams + + +class LoggingMessageNotificationParams(NotificationParams): + """Parameters for logging message notifications.""" + + level: LoggingLevel + """The severity of this log message.""" + logger: str | None = None + """An optional name of the logger issuing this message.""" + data: Any + """ + The data to be logged, such as a string message or an object. Any JSON serializable + type is allowed here. + """ + model_config = ConfigDict(extra="allow") + + +class LoggingMessageNotification(Notification[LoggingMessageNotificationParams, Literal["notifications/message"]]): + """Notification of a log message passed from server to client.""" + + method: Literal["notifications/message"] + params: LoggingMessageNotificationParams + + +IncludeContext = Literal["none", "thisServer", "allServers"] + + +class ModelHint(BaseModel): + """Hints to use for model selection.""" + + name: str | None = None + """A hint for a model name.""" + + model_config = ConfigDict(extra="allow") + + +class ModelPreferences(BaseModel): + """ + The server's preferences for model selection, requested by the client during + sampling. + + Because LLMs can vary along multiple dimensions, choosing the "best" model is + rarely straightforward. Different models excel in different areas—some are + faster but less capable, others are more capable but more expensive, and so + on. This interface allows servers to express their priorities across multiple + dimensions to help clients make an appropriate selection for their use case. + + These preferences are always advisory. The client MAY ignore them. It is also + up to the client to decide how to interpret these preferences and how to + balance them against other considerations. + """ + + hints: list[ModelHint] | None = None + """ + Optional hints to use for model selection. + + If multiple hints are specified, the client MUST evaluate them in order + (such that the first match is taken). + + The client SHOULD prioritize these hints over the numeric priorities, but + MAY still use the priorities to select from ambiguous matches. + """ + + costPriority: float | None = None + """ + How much to prioritize cost when selecting a model. A value of 0 means cost + is not important, while a value of 1 means cost is the most important + factor. + """ + + speedPriority: float | None = None + """ + How much to prioritize sampling speed (latency) when selecting a model. A + value of 0 means speed is not important, while a value of 1 means speed is + the most important factor. + """ + + intelligencePriority: float | None = None + """ + How much to prioritize intelligence and capabilities when selecting a + model. A value of 0 means intelligence is not important, while a value of 1 + means intelligence is the most important factor. + """ + + model_config = ConfigDict(extra="allow") + + +class CreateMessageRequestParams(RequestParams): + """Parameters for creating a message.""" + + messages: list[SamplingMessage] + modelPreferences: ModelPreferences | None = None + """ + The server's preferences for which model to select. The client MAY ignore + these preferences. + """ + systemPrompt: str | None = None + """An optional system prompt the server wants to use for sampling.""" + includeContext: IncludeContext | None = None + """ + A request to include context from one or more MCP servers (including the caller), to + be attached to the prompt. + """ + temperature: float | None = None + maxTokens: int + """The maximum number of tokens to sample, as requested by the server.""" + stopSequences: list[str] | None = None + metadata: dict[str, Any] | None = None + """Optional metadata to pass through to the LLM provider.""" + model_config = ConfigDict(extra="allow") + + +class CreateMessageRequest(Request[CreateMessageRequestParams, Literal["sampling/createMessage"]]): + """A request from the server to sample an LLM via the client.""" + + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams + + +StopReason = Literal["endTurn", "stopSequence", "maxTokens"] | str + + +class CreateMessageResult(Result): + """The client's response to a sampling/create_message request from the server.""" + + role: Role + content: TextContent | ImageContent + model: str + """The name of the model that generated the message.""" + stopReason: StopReason | None = None + """The reason why sampling stopped, if known.""" + + +class ResourceReference(BaseModel): + """A reference to a resource or resource template definition.""" + + type: Literal["ref/resource"] + uri: str + """The URI or URI template of the resource.""" + model_config = ConfigDict(extra="allow") + + +class PromptReference(BaseModel): + """Identifies a prompt.""" + + type: Literal["ref/prompt"] + name: str + """The name of the prompt or prompt template""" + model_config = ConfigDict(extra="allow") + + +class CompletionArgument(BaseModel): + """The argument's information for completion requests.""" + + name: str + """The name of the argument""" + value: str + """The value of the argument to use for completion matching.""" + model_config = ConfigDict(extra="allow") + + +class CompleteRequestParams(RequestParams): + """Parameters for completion requests.""" + + ref: ResourceReference | PromptReference + argument: CompletionArgument + model_config = ConfigDict(extra="allow") + + +class CompleteRequest(Request[CompleteRequestParams, Literal["completion/complete"]]): + """A request from the client to the server, to ask for completion options.""" + + method: Literal["completion/complete"] + params: CompleteRequestParams + + +class Completion(BaseModel): + """Completion information.""" + + values: list[str] + """An array of completion values. Must not exceed 100 items.""" + total: int | None = None + """ + The total number of completion options available. This can exceed the number of + values actually sent in the response. + """ + hasMore: bool | None = None + """ + Indicates whether there are additional completion options beyond those provided in + the current response, even if the exact total is unknown. + """ + model_config = ConfigDict(extra="allow") + + +class CompleteResult(Result): + """The server's response to a completion/complete request""" + + completion: Completion + + +class ListRootsRequest(Request[RequestParams | None, Literal["roots/list"]]): + """ + Sent from the server to request a list of root URIs from the client. Roots allow + servers to ask for specific directories or files to operate on. A common example + for roots is providing a set of repositories or directories a server should operate + on. + + This request is typically used when the server needs to understand the file system + structure or access specific locations that the client has permission to read from. + """ + + method: Literal["roots/list"] + params: RequestParams | None = None + + +class Root(BaseModel): + """Represents a root directory or file that the server can operate on.""" + + uri: FileUrl + """ + The URI identifying the root. This *must* start with file:// for now. + This restriction may be relaxed in future versions of the protocol to allow + other URI schemes. + """ + name: str | None = None + """ + An optional name for the root. This can be used to provide a human-readable + identifier for the root, which may be useful for display purposes or for + referencing the root in other parts of the application. + """ + model_config = ConfigDict(extra="allow") + + +class ListRootsResult(Result): + """ + The client's response to a roots/list request from the server. + This result contains an array of Root objects, each representing a root directory + or file that the server can operate on. + """ + + roots: list[Root] + + +class RootsListChangedNotification( + Notification[NotificationParams | None, Literal["notifications/roots/list_changed"]] +): + """ + A notification from the client to the server, informing it that the list of + roots has changed. + + This notification should be sent whenever the client adds, removes, or + modifies any root. The server should then request an updated list of roots + using the ListRootsRequest. + """ + + method: Literal["notifications/roots/list_changed"] + params: NotificationParams | None = None + + +class CancelledNotificationParams(NotificationParams): + """Parameters for cancellation notifications.""" + + requestId: RequestId + """The ID of the request to cancel.""" + reason: str | None = None + """An optional string describing the reason for the cancellation.""" + model_config = ConfigDict(extra="allow") + + +class CancelledNotification(Notification[CancelledNotificationParams, Literal["notifications/cancelled"]]): + """ + This notification can be sent by either side to indicate that it is canceling a + previously-issued request. + """ + + method: Literal["notifications/cancelled"] + params: CancelledNotificationParams + + +class ClientRequest( + RootModel[ + PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest + ] +): + pass + + +class ClientNotification( + RootModel[CancelledNotification | ProgressNotification | InitializedNotification | RootsListChangedNotification] +): + pass + + +class ClientResult(RootModel[EmptyResult | CreateMessageResult | ListRootsResult]): + pass + + +class ServerRequest(RootModel[PingRequest | CreateMessageRequest | ListRootsRequest]): + pass + + +class ServerNotification( + RootModel[ + CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification + ] +): + pass + + +class ServerResult( + RootModel[ + EmptyResult + | InitializeResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult + ] +): + pass + + +ResumptionToken = str + +ResumptionTokenUpdateCallback = Callable[[ResumptionToken], None] + + +@dataclass +class ClientMessageMetadata: + """Metadata specific to client messages.""" + + resumption_token: ResumptionToken | None = None + on_resumption_token_update: Callable[[ResumptionToken], None] | None = None + + +@dataclass +class ServerMessageMetadata: + """Metadata specific to server messages.""" + + related_request_id: RequestId | None = None + request_context: object | None = None + + +MessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None + + +@dataclass +class SessionMessage: + """A message with specific metadata for transport-specific features.""" + + message: JSONRPCMessage + metadata: MessageMetadata = None + + +class OAuthClientMetadata(BaseModel): + client_name: str + redirect_uris: list[str] + grant_types: Optional[list[str]] = None + response_types: Optional[list[str]] = None + token_endpoint_auth_method: Optional[str] = None + client_uri: Optional[str] = None + scope: Optional[str] = None + + +class OAuthClientInformation(BaseModel): + client_id: str + client_secret: Optional[str] = None + + +class OAuthClientInformationFull(OAuthClientInformation): + client_name: str | None = None + redirect_uris: list[str] + scope: Optional[str] = None + grant_types: Optional[list[str]] = None + response_types: Optional[list[str]] = None + token_endpoint_auth_method: Optional[str] = None + + +class OAuthTokens(BaseModel): + access_token: str + token_type: str + expires_in: Optional[int] = None + refresh_token: Optional[str] = None + scope: Optional[str] = None + + +class OAuthMetadata(BaseModel): + authorization_endpoint: str + token_endpoint: str + registration_endpoint: Optional[str] = None + response_types_supported: list[str] + grant_types_supported: Optional[list[str]] = None + code_challenge_methods_supported: Optional[list[str]] = None diff --git a/api/core/mcp/utils.py b/api/core/mcp/utils.py new file mode 100644 index 0000000000..a54badcd4c --- /dev/null +++ b/api/core/mcp/utils.py @@ -0,0 +1,114 @@ +import json + +import httpx + +from configs import dify_config +from core.mcp.types import ErrorData, JSONRPCError +from core.model_runtime.utils.encoders import jsonable_encoder + +HTTP_REQUEST_NODE_SSL_VERIFY = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY + +STATUS_FORCELIST = [429, 500, 502, 503, 504] + + +def create_ssrf_proxy_mcp_http_client( + headers: dict[str, str] | None = None, + timeout: httpx.Timeout | None = None, +) -> httpx.Client: + """Create an HTTPX client with SSRF proxy configuration for MCP connections. + + Args: + headers: Optional headers to include in the client + timeout: Optional timeout configuration + + Returns: + Configured httpx.Client with proxy settings + """ + if dify_config.SSRF_PROXY_ALL_URL: + return httpx.Client( + verify=HTTP_REQUEST_NODE_SSL_VERIFY, + headers=headers or {}, + timeout=timeout, + follow_redirects=True, + proxy=dify_config.SSRF_PROXY_ALL_URL, + ) + elif dify_config.SSRF_PROXY_HTTP_URL and dify_config.SSRF_PROXY_HTTPS_URL: + proxy_mounts = { + "http://": httpx.HTTPTransport(proxy=dify_config.SSRF_PROXY_HTTP_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY), + "https://": httpx.HTTPTransport( + proxy=dify_config.SSRF_PROXY_HTTPS_URL, verify=HTTP_REQUEST_NODE_SSL_VERIFY + ), + } + return httpx.Client( + verify=HTTP_REQUEST_NODE_SSL_VERIFY, + headers=headers or {}, + timeout=timeout, + follow_redirects=True, + mounts=proxy_mounts, + ) + else: + return httpx.Client( + verify=HTTP_REQUEST_NODE_SSL_VERIFY, + headers=headers or {}, + timeout=timeout, + follow_redirects=True, + ) + + +def ssrf_proxy_sse_connect(url, **kwargs): + """Connect to SSE endpoint with SSRF proxy protection. + + This function creates an SSE connection using the configured proxy settings + to prevent SSRF attacks when connecting to external endpoints. + + Args: + url: The SSE endpoint URL + **kwargs: Additional arguments passed to the SSE connection + + Returns: + EventSource object for SSE streaming + """ + from httpx_sse import connect_sse + + # Extract client if provided, otherwise create one + client = kwargs.pop("client", None) + if client is None: + # Create client with SSRF proxy configuration + timeout = kwargs.pop( + "timeout", + httpx.Timeout( + timeout=dify_config.SSRF_DEFAULT_TIME_OUT, + connect=dify_config.SSRF_DEFAULT_CONNECT_TIME_OUT, + read=dify_config.SSRF_DEFAULT_READ_TIME_OUT, + write=dify_config.SSRF_DEFAULT_WRITE_TIME_OUT, + ), + ) + headers = kwargs.pop("headers", {}) + client = create_ssrf_proxy_mcp_http_client(headers=headers, timeout=timeout) + client_provided = False + else: + client_provided = True + + # Extract method if provided, default to GET + method = kwargs.pop("method", "GET") + + try: + return connect_sse(client, method, url, **kwargs) + except Exception: + # If we created the client, we need to clean it up on error + if not client_provided: + client.close() + raise + + +def create_mcp_error_response(request_id: int | str | None, code: int, message: str, data=None): + """Create MCP error response""" + error_data = ErrorData(code=code, message=message, data=data) + json_response = JSONRPCError( + jsonrpc="2.0", + id=request_id or 1, + error=error_data, + ) + json_data = json.dumps(jsonable_encoder(json_response)) + sse_content = f"event: message\ndata: {json_data}\n\n".encode() + yield sse_content diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index 2254b3d4d5..a9f0a92e5d 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -1,6 +1,8 @@ from collections.abc import Sequence from typing import Optional +from sqlalchemy import select + from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.file import file_manager from core.model_manager import ModelInstance @@ -17,11 +19,15 @@ from core.prompt.utils.extract_thread_messages import extract_thread_messages from extensions.ext_database import db from factories import file_factory from models.model import AppMode, Conversation, Message, MessageFile -from models.workflow import WorkflowRun +from models.workflow import Workflow, WorkflowRun class TokenBufferMemory: - def __init__(self, conversation: Conversation, model_instance: ModelInstance) -> None: + def __init__( + self, + conversation: Conversation, + model_instance: ModelInstance, + ) -> None: self.conversation = conversation self.model_instance = model_instance @@ -36,20 +42,8 @@ class TokenBufferMemory: app_record = self.conversation.app # fetch limited messages, and return reversed - query = ( - db.session.query( - Message.id, - Message.query, - Message.answer, - Message.created_at, - Message.workflow_run_id, - Message.parent_message_id, - Message.answer_tokens, - ) - .filter( - Message.conversation_id == self.conversation.id, - ) - .order_by(Message.created_at.desc()) + stmt = ( + select(Message).where(Message.conversation_id == self.conversation.id).order_by(Message.created_at.desc()) ) if message_limit and message_limit > 0: @@ -57,7 +51,9 @@ class TokenBufferMemory: else: message_limit = 500 - messages = query.limit(message_limit).all() + stmt = stmt.limit(message_limit) + + messages = db.session.scalars(stmt).all() # instead of all messages from the conversation, we only need to extract messages # that belong to the thread of last message @@ -74,18 +70,20 @@ class TokenBufferMemory: files = db.session.query(MessageFile).filter(MessageFile.message_id == message.id).all() if files: file_extra_config = None - if self.conversation.mode not in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: + if self.conversation.mode in {AppMode.AGENT_CHAT, AppMode.COMPLETION, AppMode.CHAT}: file_extra_config = FileUploadConfigManager.convert(self.conversation.model_config) + elif self.conversation.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}: + workflow_run = db.session.scalar( + select(WorkflowRun).where(WorkflowRun.id == message.workflow_run_id) + ) + if not workflow_run: + raise ValueError(f"Workflow run not found: {message.workflow_run_id}") + workflow = db.session.scalar(select(Workflow).where(Workflow.id == workflow_run.workflow_id)) + if not workflow: + raise ValueError(f"Workflow not found: {workflow_run.workflow_id}") + file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) else: - if message.workflow_run_id: - workflow_run = ( - db.session.query(WorkflowRun).filter(WorkflowRun.id == message.workflow_run_id).first() - ) - - if workflow_run and workflow_run.workflow: - file_extra_config = FileUploadConfigManager.convert( - workflow_run.workflow.features_dict, is_vision=False - ) + raise AssertionError(f"Invalid app mode: {self.conversation.mode}") detail = ImagePromptMessageContent.DETAIL.LOW if file_extra_config and app_record: diff --git a/api/core/model_manager.py b/api/core/model_manager.py index 995a30d44c..4886ffe244 100644 --- a/api/core/model_manager.py +++ b/api/core/model_manager.py @@ -542,8 +542,6 @@ class LBModelManager: return config - return None - def cooldown(self, config: ModelLoadBalancingConfiguration, expire: int = 60) -> None: """ Cooldown model load balancing config diff --git a/api/core/model_runtime/entities/llm_entities.py b/api/core/model_runtime/entities/llm_entities.py index de5a748d4f..ace2c1f770 100644 --- a/api/core/model_runtime/entities/llm_entities.py +++ b/api/core/model_runtime/entities/llm_entities.py @@ -1,7 +1,7 @@ -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from decimal import Decimal from enum import StrEnum -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel, Field @@ -53,6 +53,37 @@ class LLMUsage(ModelUsage): latency=0.0, ) + @classmethod + def from_metadata(cls, metadata: dict) -> "LLMUsage": + """ + Create LLMUsage instance from metadata dictionary with default values. + + Args: + metadata: Dictionary containing usage metadata + + Returns: + LLMUsage instance with values from metadata or defaults + """ + total_tokens = metadata.get("total_tokens", 0) + completion_tokens = metadata.get("completion_tokens", 0) + if total_tokens > 0 and completion_tokens == 0: + completion_tokens = total_tokens + + return cls( + prompt_tokens=metadata.get("prompt_tokens", 0), + completion_tokens=completion_tokens, + total_tokens=total_tokens, + prompt_unit_price=Decimal(str(metadata.get("prompt_unit_price", 0))), + completion_unit_price=Decimal(str(metadata.get("completion_unit_price", 0))), + total_price=Decimal(str(metadata.get("total_price", 0))), + currency=metadata.get("currency", "USD"), + prompt_price_unit=Decimal(str(metadata.get("prompt_price_unit", 0))), + completion_price_unit=Decimal(str(metadata.get("completion_price_unit", 0))), + prompt_price=Decimal(str(metadata.get("prompt_price", 0))), + completion_price=Decimal(str(metadata.get("completion_price", 0))), + latency=metadata.get("latency", 0.0), + ) + def plus(self, other: "LLMUsage") -> "LLMUsage": """ Add two LLMUsage instances together. @@ -101,6 +132,20 @@ class LLMResult(BaseModel): system_fingerprint: Optional[str] = None +class LLMStructuredOutput(BaseModel): + """ + Model class for llm structured output. + """ + + structured_output: Optional[Mapping[str, Any]] = None + + +class LLMResultWithStructuredOutput(LLMResult, LLMStructuredOutput): + """ + Model class for llm result with structured output. + """ + + class LLMResultChunkDelta(BaseModel): """ Model class for llm result chunk delta. @@ -123,6 +168,12 @@ class LLMResultChunk(BaseModel): delta: LLMResultChunkDelta +class LLMResultChunkWithStructuredOutput(LLMResultChunk, LLMStructuredOutput): + """ + Model class for llm result chunk with structured output. + """ + + class NumTokensResult(PriceInfo): """ Model class for number of tokens result. diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index d0f9ee13e5..c9aa8d1474 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -123,6 +123,8 @@ class ProviderEntity(BaseModel): description: Optional[I18nObject] = None icon_small: Optional[I18nObject] = None icon_large: Optional[I18nObject] = None + icon_small_dark: Optional[I18nObject] = None + icon_large_dark: Optional[I18nObject] = None background: Optional[str] = None help: Optional[ProviderHelpEntity] = None supported_model_types: Sequence[ModelType] diff --git a/api/core/ops/aliyun_trace/__init__.py b/api/core/ops/aliyun_trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/aliyun_trace/aliyun_trace.py b/api/core/ops/aliyun_trace/aliyun_trace.py new file mode 100644 index 0000000000..db8fec4ee9 --- /dev/null +++ b/api/core/ops/aliyun_trace/aliyun_trace.py @@ -0,0 +1,488 @@ +import json +import logging +from collections.abc import Sequence +from typing import Optional +from urllib.parse import urljoin + +from opentelemetry.trace import Status, StatusCode +from sqlalchemy.orm import Session, sessionmaker + +from core.ops.aliyun_trace.data_exporter.traceclient import ( + TraceClient, + convert_datetime_to_nanoseconds, + convert_to_span_id, + convert_to_trace_id, + generate_span_id, +) +from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData +from core.ops.aliyun_trace.entities.semconv import ( + GEN_AI_COMPLETION, + GEN_AI_FRAMEWORK, + GEN_AI_MODEL_NAME, + GEN_AI_PROMPT, + GEN_AI_PROMPT_TEMPLATE_TEMPLATE, + GEN_AI_PROMPT_TEMPLATE_VARIABLE, + GEN_AI_RESPONSE_FINISH_REASON, + GEN_AI_SESSION_ID, + GEN_AI_SPAN_KIND, + GEN_AI_SYSTEM, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, + GEN_AI_USER_ID, + INPUT_VALUE, + OUTPUT_VALUE, + RETRIEVAL_DOCUMENT, + RETRIEVAL_QUERY, + TOOL_DESCRIPTION, + TOOL_NAME, + TOOL_PARAMETERS, + GenAISpanKind, +) +from core.ops.base_trace_instance import BaseTraceInstance +from core.ops.entities.config_entity import AliyunConfig +from core.ops.entities.trace_entity import ( + BaseTraceInfo, + DatasetRetrievalTraceInfo, + GenerateNameTraceInfo, + MessageTraceInfo, + ModerationTraceInfo, + SuggestedQuestionTraceInfo, + ToolTraceInfo, + WorkflowTraceInfo, +) +from core.rag.models.document import Document +from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.workflow.entities.workflow_node_execution import ( + WorkflowNodeExecution, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from core.workflow.nodes import NodeType +from models import Account, App, EndUser, TenantAccountJoin, WorkflowNodeExecutionTriggeredFrom, db + +logger = logging.getLogger(__name__) + + +class AliyunDataTrace(BaseTraceInstance): + def __init__( + self, + aliyun_config: AliyunConfig, + ): + super().__init__(aliyun_config) + base_url = aliyun_config.endpoint.rstrip("/") + endpoint = urljoin(base_url, f"adapt_{aliyun_config.license_key}/api/otlp/traces") + self.trace_client = TraceClient(service_name=aliyun_config.app_name, endpoint=endpoint) + + def trace(self, trace_info: BaseTraceInfo): + if isinstance(trace_info, WorkflowTraceInfo): + self.workflow_trace(trace_info) + if isinstance(trace_info, MessageTraceInfo): + self.message_trace(trace_info) + if isinstance(trace_info, ModerationTraceInfo): + pass + if isinstance(trace_info, SuggestedQuestionTraceInfo): + self.suggested_question_trace(trace_info) + if isinstance(trace_info, DatasetRetrievalTraceInfo): + self.dataset_retrieval_trace(trace_info) + if isinstance(trace_info, ToolTraceInfo): + self.tool_trace(trace_info) + if isinstance(trace_info, GenerateNameTraceInfo): + pass + + def api_check(self): + return self.trace_client.api_check() + + def get_project_url(self): + try: + return self.trace_client.get_project_url() + except Exception as e: + logger.info(f"Aliyun get run url failed: {str(e)}", exc_info=True) + raise ValueError(f"Aliyun get run url failed: {str(e)}") + + def workflow_trace(self, trace_info: WorkflowTraceInfo): + trace_id = convert_to_trace_id(trace_info.workflow_run_id) + workflow_span_id = convert_to_span_id(trace_info.workflow_run_id, "workflow") + self.add_workflow_span(trace_id, workflow_span_id, trace_info) + + workflow_node_executions = self.get_workflow_node_executions(trace_info) + for node_execution in workflow_node_executions: + node_span = self.build_workflow_node_span(node_execution, trace_id, trace_info, workflow_span_id) + self.trace_client.add_span(node_span) + + def message_trace(self, trace_info: MessageTraceInfo): + message_data = trace_info.message_data + if message_data is None: + return + message_id = trace_info.message_id + + user_id = message_data.from_account_id + if message_data.from_end_user_id: + end_user_data: Optional[EndUser] = ( + db.session.query(EndUser).filter(EndUser.id == message_data.from_end_user_id).first() + ) + if end_user_data is not None: + user_id = end_user_data.session_id + + status: Status = Status(StatusCode.OK) + if trace_info.error: + status = Status(StatusCode.ERROR, trace_info.error) + + trace_id = convert_to_trace_id(message_id) + message_span_id = convert_to_span_id(message_id, "message") + message_span = SpanData( + trace_id=trace_id, + parent_span_id=None, + span_id=message_span_id, + name="message", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""), + GEN_AI_USER_ID: str(user_id), + GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value, + GEN_AI_FRAMEWORK: "dify", + INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + OUTPUT_VALUE: str(trace_info.outputs), + }, + status=status, + ) + self.trace_client.add_span(message_span) + + app_model_config = getattr(trace_info.message_data, "app_model_config", {}) + pre_prompt = getattr(app_model_config, "pre_prompt", "") + inputs_data = getattr(trace_info.message_data, "inputs", {}) + llm_span = SpanData( + trace_id=trace_id, + parent_span_id=message_span_id, + span_id=convert_to_span_id(message_id, "llm"), + name="llm", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id", ""), + GEN_AI_USER_ID: str(user_id), + GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value, + GEN_AI_FRAMEWORK: "dify", + GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name", ""), + GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider", ""), + GEN_AI_USAGE_INPUT_TOKENS: str(trace_info.message_tokens), + GEN_AI_USAGE_OUTPUT_TOKENS: str(trace_info.answer_tokens), + GEN_AI_USAGE_TOTAL_TOKENS: str(trace_info.total_tokens), + GEN_AI_PROMPT_TEMPLATE_VARIABLE: json.dumps(inputs_data, ensure_ascii=False), + GEN_AI_PROMPT_TEMPLATE_TEMPLATE: pre_prompt, + GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False), + GEN_AI_COMPLETION: str(trace_info.outputs), + INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + OUTPUT_VALUE: str(trace_info.outputs), + }, + status=status, + ) + self.trace_client.add_span(llm_span) + + def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo): + if trace_info.message_data is None: + return + message_id = trace_info.message_id + + documents_data = extract_retrieval_documents(trace_info.documents) + dataset_retrieval_span = SpanData( + trace_id=convert_to_trace_id(message_id), + parent_span_id=convert_to_span_id(message_id, "message"), + span_id=generate_span_id(), + name="dataset_retrieval", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value, + GEN_AI_FRAMEWORK: "dify", + RETRIEVAL_QUERY: str(trace_info.inputs), + RETRIEVAL_DOCUMENT: json.dumps(documents_data, ensure_ascii=False), + INPUT_VALUE: str(trace_info.inputs), + OUTPUT_VALUE: json.dumps(documents_data, ensure_ascii=False), + }, + ) + self.trace_client.add_span(dataset_retrieval_span) + + def tool_trace(self, trace_info: ToolTraceInfo): + if trace_info.message_data is None: + return + message_id = trace_info.message_id + + status: Status = Status(StatusCode.OK) + if trace_info.error: + status = Status(StatusCode.ERROR, trace_info.error) + + tool_span = SpanData( + trace_id=convert_to_trace_id(message_id), + parent_span_id=convert_to_span_id(message_id, "message"), + span_id=generate_span_id(), + name=trace_info.tool_name, + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value, + GEN_AI_FRAMEWORK: "dify", + TOOL_NAME: trace_info.tool_name, + TOOL_DESCRIPTION: json.dumps(trace_info.tool_config, ensure_ascii=False), + TOOL_PARAMETERS: json.dumps(trace_info.tool_inputs, ensure_ascii=False), + INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + OUTPUT_VALUE: str(trace_info.tool_outputs), + }, + status=status, + ) + self.trace_client.add_span(tool_span) + + def get_workflow_node_executions(self, trace_info: WorkflowTraceInfo) -> Sequence[WorkflowNodeExecution]: + # through workflow_run_id get all_nodes_execution using repository + session_factory = sessionmaker(bind=db.engine) + # Find the app's creator account + with Session(db.engine, expire_on_commit=False) as session: + # Get the app to find its creator + app_id = trace_info.metadata.get("app_id") + if not app_id: + raise ValueError("No app_id found in trace_info metadata") + + app = session.query(App).filter(App.id == app_id).first() + if not app: + raise ValueError(f"App with id {app_id} not found") + + if not app.created_by: + raise ValueError(f"App with id {app_id} has no creator (created_by is None)") + + service_account = session.query(Account).filter(Account.id == app.created_by).first() + if not service_account: + raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}") + current_tenant = ( + session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first() + ) + if not current_tenant: + raise ValueError(f"Current tenant not found for account {service_account.id}") + service_account.set_tenant_id(current_tenant.tenant_id) + workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + session_factory=session_factory, + user=service_account, + app_id=trace_info.metadata.get("app_id"), + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, + ) + # Get all executions for this workflow run + workflow_node_executions = workflow_node_execution_repository.get_by_workflow_run( + workflow_run_id=trace_info.workflow_run_id + ) + return workflow_node_executions + + def build_workflow_node_span( + self, node_execution: WorkflowNodeExecution, trace_id: int, trace_info: WorkflowTraceInfo, workflow_span_id: int + ): + try: + if node_execution.node_type == NodeType.LLM: + node_span = self.build_workflow_llm_span(trace_id, workflow_span_id, trace_info, node_execution) + elif node_execution.node_type == NodeType.KNOWLEDGE_RETRIEVAL: + node_span = self.build_workflow_retrieval_span(trace_id, workflow_span_id, trace_info, node_execution) + elif node_execution.node_type == NodeType.TOOL: + node_span = self.build_workflow_tool_span(trace_id, workflow_span_id, trace_info, node_execution) + else: + node_span = self.build_workflow_task_span(trace_id, workflow_span_id, trace_info, node_execution) + return node_span + except Exception as e: + logging.debug(f"Error occurred in build_workflow_node_span: {e}", exc_info=True) + return None + + def get_workflow_node_status(self, node_execution: WorkflowNodeExecution) -> Status: + span_status: Status = Status(StatusCode.UNSET) + if node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED: + span_status = Status(StatusCode.OK) + elif node_execution.status in [WorkflowNodeExecutionStatus.FAILED, WorkflowNodeExecutionStatus.EXCEPTION]: + span_status = Status(StatusCode.ERROR, str(node_execution.error)) + return span_status + + def build_workflow_task_span( + self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution + ) -> SpanData: + return SpanData( + trace_id=trace_id, + parent_span_id=workflow_span_id, + span_id=convert_to_span_id(node_execution.id, "node"), + name=node_execution.title, + start_time=convert_datetime_to_nanoseconds(node_execution.created_at), + end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), + attributes={ + GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", + GEN_AI_SPAN_KIND: GenAISpanKind.TASK.value, + GEN_AI_FRAMEWORK: "dify", + INPUT_VALUE: json.dumps(node_execution.inputs, ensure_ascii=False), + OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False), + }, + status=self.get_workflow_node_status(node_execution), + ) + + def build_workflow_tool_span( + self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution + ) -> SpanData: + tool_des = {} + if node_execution.metadata: + tool_des = node_execution.metadata.get(WorkflowNodeExecutionMetadataKey.TOOL_INFO, {}) + return SpanData( + trace_id=trace_id, + parent_span_id=workflow_span_id, + span_id=convert_to_span_id(node_execution.id, "node"), + name=node_execution.title, + start_time=convert_datetime_to_nanoseconds(node_execution.created_at), + end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), + attributes={ + GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value, + GEN_AI_FRAMEWORK: "dify", + TOOL_NAME: node_execution.title, + TOOL_DESCRIPTION: json.dumps(tool_des, ensure_ascii=False), + TOOL_PARAMETERS: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False), + INPUT_VALUE: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False), + OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False), + }, + status=self.get_workflow_node_status(node_execution), + ) + + def build_workflow_retrieval_span( + self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution + ) -> SpanData: + input_value = "" + if node_execution.inputs: + input_value = str(node_execution.inputs.get("query", "")) + output_value = "" + if node_execution.outputs: + output_value = json.dumps(node_execution.outputs.get("result", []), ensure_ascii=False) + return SpanData( + trace_id=trace_id, + parent_span_id=workflow_span_id, + span_id=convert_to_span_id(node_execution.id, "node"), + name=node_execution.title, + start_time=convert_datetime_to_nanoseconds(node_execution.created_at), + end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), + attributes={ + GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value, + GEN_AI_FRAMEWORK: "dify", + RETRIEVAL_QUERY: input_value, + RETRIEVAL_DOCUMENT: output_value, + INPUT_VALUE: input_value, + OUTPUT_VALUE: output_value, + }, + status=self.get_workflow_node_status(node_execution), + ) + + def build_workflow_llm_span( + self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution + ) -> SpanData: + process_data = node_execution.process_data or {} + outputs = node_execution.outputs or {} + usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + return SpanData( + trace_id=trace_id, + parent_span_id=workflow_span_id, + span_id=convert_to_span_id(node_execution.id, "node"), + name=node_execution.title, + start_time=convert_datetime_to_nanoseconds(node_execution.created_at), + end_time=convert_datetime_to_nanoseconds(node_execution.finished_at), + attributes={ + GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", + GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value, + GEN_AI_FRAMEWORK: "dify", + GEN_AI_MODEL_NAME: process_data.get("model_name", ""), + GEN_AI_SYSTEM: process_data.get("model_provider", ""), + GEN_AI_USAGE_INPUT_TOKENS: str(usage_data.get("prompt_tokens", 0)), + GEN_AI_USAGE_OUTPUT_TOKENS: str(usage_data.get("completion_tokens", 0)), + GEN_AI_USAGE_TOTAL_TOKENS: str(usage_data.get("total_tokens", 0)), + GEN_AI_PROMPT: json.dumps(process_data.get("prompts", []), ensure_ascii=False), + GEN_AI_COMPLETION: str(outputs.get("text", "")), + GEN_AI_RESPONSE_FINISH_REASON: outputs.get("finish_reason", ""), + INPUT_VALUE: json.dumps(process_data.get("prompts", []), ensure_ascii=False), + OUTPUT_VALUE: str(outputs.get("text", "")), + }, + status=self.get_workflow_node_status(node_execution), + ) + + def add_workflow_span(self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo): + message_span_id = None + if trace_info.message_id: + message_span_id = convert_to_span_id(trace_info.message_id, "message") + user_id = trace_info.metadata.get("user_id") + status: Status = Status(StatusCode.OK) + if trace_info.error: + status = Status(StatusCode.ERROR, trace_info.error) + if message_span_id: # chatflow + message_span = SpanData( + trace_id=trace_id, + parent_span_id=None, + span_id=message_span_id, + name="message", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "", + GEN_AI_USER_ID: str(user_id), + GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value, + GEN_AI_FRAMEWORK: "dify", + INPUT_VALUE: trace_info.workflow_run_inputs.get("sys.query", ""), + OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False), + }, + status=status, + ) + self.trace_client.add_span(message_span) + + workflow_span = SpanData( + trace_id=trace_id, + parent_span_id=message_span_id, + span_id=workflow_span_id, + name="workflow", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_USER_ID: str(user_id), + GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value, + GEN_AI_FRAMEWORK: "dify", + INPUT_VALUE: json.dumps(trace_info.workflow_run_inputs, ensure_ascii=False), + OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False), + }, + status=status, + ) + self.trace_client.add_span(workflow_span) + + def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo): + message_id = trace_info.message_id + status: Status = Status(StatusCode.OK) + if trace_info.error: + status = Status(StatusCode.ERROR, trace_info.error) + suggested_question_span = SpanData( + trace_id=convert_to_trace_id(message_id), + parent_span_id=convert_to_span_id(message_id, "message"), + span_id=convert_to_span_id(message_id, "suggested_question"), + name="suggested_question", + start_time=convert_datetime_to_nanoseconds(trace_info.start_time), + end_time=convert_datetime_to_nanoseconds(trace_info.end_time), + attributes={ + GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value, + GEN_AI_FRAMEWORK: "dify", + GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name", ""), + GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider", ""), + GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False), + GEN_AI_COMPLETION: json.dumps(trace_info.suggested_question, ensure_ascii=False), + INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + OUTPUT_VALUE: json.dumps(trace_info.suggested_question, ensure_ascii=False), + }, + status=status, + ) + self.trace_client.add_span(suggested_question_span) + + +def extract_retrieval_documents(documents: list[Document]): + documents_data = [] + for document in documents: + document_data = { + "content": document.page_content, + "metadata": { + "dataset_id": document.metadata.get("dataset_id"), + "doc_id": document.metadata.get("doc_id"), + "document_id": document.metadata.get("document_id"), + }, + "score": document.metadata.get("score"), + } + documents_data.append(document_data) + return documents_data diff --git a/api/core/ops/aliyun_trace/data_exporter/__init__.py b/api/core/ops/aliyun_trace/data_exporter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/aliyun_trace/data_exporter/traceclient.py b/api/core/ops/aliyun_trace/data_exporter/traceclient.py new file mode 100644 index 0000000000..ba5ac3f420 --- /dev/null +++ b/api/core/ops/aliyun_trace/data_exporter/traceclient.py @@ -0,0 +1,200 @@ +import hashlib +import logging +import random +import socket +import threading +import uuid +from collections import deque +from collections.abc import Sequence +from datetime import datetime +from typing import Optional + +import requests +from opentelemetry import trace as trace_api +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.semconv.resource import ResourceAttributes + +from configs import dify_config +from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData + +INVALID_SPAN_ID = 0x0000000000000000 +INVALID_TRACE_ID = 0x00000000000000000000000000000000 + +logger = logging.getLogger(__name__) + + +class TraceClient: + def __init__( + self, + service_name: str, + endpoint: str, + max_queue_size: int = 1000, + schedule_delay_sec: int = 5, + max_export_batch_size: int = 50, + ): + self.endpoint = endpoint + self.resource = Resource( + attributes={ + ResourceAttributes.SERVICE_NAME: service_name, + ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", + ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", + ResourceAttributes.HOST_NAME: socket.gethostname(), + } + ) + self.span_builder = SpanBuilder(self.resource) + self.exporter = OTLPSpanExporter(endpoint=endpoint) + + self.max_queue_size = max_queue_size + self.schedule_delay_sec = schedule_delay_sec + self.max_export_batch_size = max_export_batch_size + + self.queue: deque = deque(maxlen=max_queue_size) + self.condition = threading.Condition(threading.Lock()) + self.done = False + + self.worker_thread = threading.Thread(target=self._worker, daemon=True) + self.worker_thread.start() + + self._spans_dropped = False + + def export(self, spans: Sequence[ReadableSpan]): + self.exporter.export(spans) + + def api_check(self): + try: + response = requests.head(self.endpoint, timeout=5) + if response.status_code == 405: + return True + else: + logger.debug(f"AliyunTrace API check failed: Unexpected status code: {response.status_code}") + return False + except requests.exceptions.RequestException as e: + logger.debug(f"AliyunTrace API check failed: {str(e)}") + raise ValueError(f"AliyunTrace API check failed: {str(e)}") + + def get_project_url(self): + return "https://arms.console.aliyun.com/#/llm" + + def add_span(self, span_data: SpanData): + if span_data is None: + return + span: ReadableSpan = self.span_builder.build_span(span_data) + with self.condition: + if len(self.queue) == self.max_queue_size: + if not self._spans_dropped: + logger.warning("Queue is full, likely spans will be dropped.") + self._spans_dropped = True + + self.queue.appendleft(span) + if len(self.queue) >= self.max_export_batch_size: + self.condition.notify() + + def _worker(self): + while not self.done: + with self.condition: + if len(self.queue) < self.max_export_batch_size and not self.done: + self.condition.wait(timeout=self.schedule_delay_sec) + self._export_batch() + + def _export_batch(self): + spans_to_export: list[ReadableSpan] = [] + with self.condition: + while len(spans_to_export) < self.max_export_batch_size and self.queue: + spans_to_export.append(self.queue.pop()) + + if spans_to_export: + try: + self.exporter.export(spans_to_export) + except Exception as e: + logger.debug(f"Error exporting spans: {e}") + + def shutdown(self): + with self.condition: + self.done = True + self.condition.notify_all() + self.worker_thread.join() + self._export_batch() + self.exporter.shutdown() + + +class SpanBuilder: + def __init__(self, resource): + self.resource = resource + self.instrumentation_scope = InstrumentationScope( + __name__, + "", + None, + None, + ) + + def build_span(self, span_data: SpanData) -> ReadableSpan: + span_context = trace_api.SpanContext( + trace_id=span_data.trace_id, + span_id=span_data.span_id, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + trace_state=None, + ) + + parent_span_context = None + if span_data.parent_span_id is not None: + parent_span_context = trace_api.SpanContext( + trace_id=span_data.trace_id, + span_id=span_data.parent_span_id, + is_remote=False, + trace_flags=trace_api.TraceFlags(trace_api.TraceFlags.SAMPLED), + trace_state=None, + ) + + span = ReadableSpan( + name=span_data.name, + context=span_context, + parent=parent_span_context, + resource=self.resource, + attributes=span_data.attributes, + events=span_data.events, + links=span_data.links, + kind=trace_api.SpanKind.INTERNAL, + status=span_data.status, + start_time=span_data.start_time, + end_time=span_data.end_time, + instrumentation_scope=self.instrumentation_scope, + ) + return span + + +def generate_span_id() -> int: + span_id = random.getrandbits(64) + while span_id == INVALID_SPAN_ID: + span_id = random.getrandbits(64) + return span_id + + +def convert_to_trace_id(uuid_v4: Optional[str]) -> int: + try: + uuid_obj = uuid.UUID(uuid_v4) + return uuid_obj.int + except Exception as e: + raise ValueError(f"Invalid UUID input: {e}") + + +def convert_to_span_id(uuid_v4: Optional[str], span_type: str) -> int: + try: + uuid_obj = uuid.UUID(uuid_v4) + except Exception as e: + raise ValueError(f"Invalid UUID input: {e}") + combined_key = f"{uuid_obj.hex}-{span_type}" + hash_bytes = hashlib.sha256(combined_key.encode("utf-8")).digest() + span_id = int.from_bytes(hash_bytes[:8], byteorder="big", signed=False) + return span_id + + +def convert_datetime_to_nanoseconds(start_time_a: Optional[datetime]) -> Optional[int]: + if start_time_a is None: + return None + timestamp_in_seconds = start_time_a.timestamp() + timestamp_in_nanoseconds = int(timestamp_in_seconds * 1e9) + return timestamp_in_nanoseconds diff --git a/api/core/ops/aliyun_trace/entities/__init__.py b/api/core/ops/aliyun_trace/entities/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/aliyun_trace/entities/aliyun_trace_entity.py b/api/core/ops/aliyun_trace/entities/aliyun_trace_entity.py new file mode 100644 index 0000000000..1caa822cd0 --- /dev/null +++ b/api/core/ops/aliyun_trace/entities/aliyun_trace_entity.py @@ -0,0 +1,21 @@ +from collections.abc import Sequence +from typing import Optional + +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import Event, Status, StatusCode +from pydantic import BaseModel, Field + + +class SpanData(BaseModel): + model_config = {"arbitrary_types_allowed": True} + + trace_id: int = Field(..., description="The unique identifier for the trace.") + parent_span_id: Optional[int] = Field(None, description="The ID of the parent span, if any.") + span_id: int = Field(..., description="The unique identifier for this span.") + name: str = Field(..., description="The name of the span.") + attributes: dict[str, str] = Field(default_factory=dict, description="Attributes associated with the span.") + events: Sequence[Event] = Field(default_factory=list, description="Events recorded in the span.") + links: Sequence[trace_api.Link] = Field(default_factory=list, description="Links to other spans.") + status: Status = Field(default=Status(StatusCode.UNSET), description="The status of the span.") + start_time: Optional[int] = Field(..., description="The start time of the span in nanoseconds.") + end_time: Optional[int] = Field(..., description="The end time of the span in nanoseconds.") diff --git a/api/core/ops/aliyun_trace/entities/semconv.py b/api/core/ops/aliyun_trace/entities/semconv.py new file mode 100644 index 0000000000..5d70264320 --- /dev/null +++ b/api/core/ops/aliyun_trace/entities/semconv.py @@ -0,0 +1,64 @@ +from enum import Enum + +# public +GEN_AI_SESSION_ID = "gen_ai.session.id" + +GEN_AI_USER_ID = "gen_ai.user.id" + +GEN_AI_USER_NAME = "gen_ai.user.name" + +GEN_AI_SPAN_KIND = "gen_ai.span.kind" + +GEN_AI_FRAMEWORK = "gen_ai.framework" + + +# Chain +INPUT_VALUE = "input.value" + +OUTPUT_VALUE = "output.value" + + +# Retriever +RETRIEVAL_QUERY = "retrieval.query" + +RETRIEVAL_DOCUMENT = "retrieval.document" + + +# LLM +GEN_AI_MODEL_NAME = "gen_ai.model_name" + +GEN_AI_SYSTEM = "gen_ai.system" + +GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + +GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + +GEN_AI_USAGE_TOTAL_TOKENS = "gen_ai.usage.total_tokens" + +GEN_AI_PROMPT_TEMPLATE_TEMPLATE = "gen_ai.prompt_template.template" + +GEN_AI_PROMPT_TEMPLATE_VARIABLE = "gen_ai.prompt_template.variable" + +GEN_AI_PROMPT = "gen_ai.prompt" + +GEN_AI_COMPLETION = "gen_ai.completion" + +GEN_AI_RESPONSE_FINISH_REASON = "gen_ai.response.finish_reason" + +# Tool +TOOL_NAME = "tool.name" + +TOOL_DESCRIPTION = "tool.description" + +TOOL_PARAMETERS = "tool.parameters" + + +class GenAISpanKind(Enum): + CHAIN = "CHAIN" + RETRIEVER = "RETRIEVER" + RERANKER = "RERANKER" + LLM = "LLM" + EMBEDDING = "EMBEDDING" + TOOL = "TOOL" + AGENT = "AGENT" + TASK = "TASK" diff --git a/api/core/ops/arize_phoenix_trace/__init__.py b/api/core/ops/arize_phoenix_trace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py new file mode 100644 index 0000000000..ffda0885d4 --- /dev/null +++ b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py @@ -0,0 +1,726 @@ +import hashlib +import json +import logging +import os +from datetime import datetime, timedelta +from typing import Optional, Union, cast + +from openinference.semconv.trace import OpenInferenceSpanKindValues, SpanAttributes +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GrpcOTLPSpanExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HttpOTLPSpanExporter +from opentelemetry.sdk import trace as trace_sdk +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.trace import SpanContext, TraceFlags, TraceState + +from core.ops.base_trace_instance import BaseTraceInstance +from core.ops.entities.config_entity import ArizeConfig, PhoenixConfig +from core.ops.entities.trace_entity import ( + BaseTraceInfo, + DatasetRetrievalTraceInfo, + GenerateNameTraceInfo, + MessageTraceInfo, + ModerationTraceInfo, + SuggestedQuestionTraceInfo, + ToolTraceInfo, + TraceTaskName, + WorkflowTraceInfo, +) +from extensions.ext_database import db +from models.model import EndUser, MessageFile +from models.workflow import WorkflowNodeExecutionModel + +logger = logging.getLogger(__name__) + + +def setup_tracer(arize_phoenix_config: ArizeConfig | PhoenixConfig) -> tuple[trace_sdk.Tracer, SimpleSpanProcessor]: + """Configure OpenTelemetry tracer with OTLP exporter for Arize/Phoenix.""" + try: + # Choose the appropriate exporter based on config type + exporter: Union[GrpcOTLPSpanExporter, HttpOTLPSpanExporter] + if isinstance(arize_phoenix_config, ArizeConfig): + arize_endpoint = f"{arize_phoenix_config.endpoint}/v1" + arize_headers = { + "api_key": arize_phoenix_config.api_key or "", + "space_id": arize_phoenix_config.space_id or "", + "authorization": f"Bearer {arize_phoenix_config.api_key or ''}", + } + exporter = GrpcOTLPSpanExporter( + endpoint=arize_endpoint, + headers=arize_headers, + timeout=30, + ) + else: + phoenix_endpoint = f"{arize_phoenix_config.endpoint}/v1/traces" + phoenix_headers = { + "api_key": arize_phoenix_config.api_key or "", + "authorization": f"Bearer {arize_phoenix_config.api_key or ''}", + } + exporter = HttpOTLPSpanExporter( + endpoint=phoenix_endpoint, + headers=phoenix_headers, + timeout=30, + ) + + attributes = { + "openinference.project.name": arize_phoenix_config.project or "", + "model_id": arize_phoenix_config.project or "", + } + resource = Resource(attributes=attributes) + provider = trace_sdk.TracerProvider(resource=resource) + processor = SimpleSpanProcessor( + exporter, + ) + provider.add_span_processor(processor) + + # Create a named tracer instead of setting the global provider + tracer_name = f"arize_phoenix_tracer_{arize_phoenix_config.project}" + logger.info(f"[Arize/Phoenix] Created tracer with name: {tracer_name}") + return cast(trace_sdk.Tracer, provider.get_tracer(tracer_name)), processor + except Exception as e: + logger.error(f"[Arize/Phoenix] Failed to setup the tracer: {str(e)}", exc_info=True) + raise + + +def datetime_to_nanos(dt: Optional[datetime]) -> int: + """Convert datetime to nanoseconds since epoch. If None, use current time.""" + if dt is None: + dt = datetime.now() + return int(dt.timestamp() * 1_000_000_000) + + +def uuid_to_trace_id(string: Optional[str]) -> int: + """Convert UUID string to a valid trace ID (16-byte integer).""" + if string is None: + string = "" + hash_object = hashlib.sha256(string.encode()) + + # Take the first 16 bytes (128 bits) of the hash + digest = hash_object.digest()[:16] + + # Convert to integer (128 bits) + return int.from_bytes(digest, byteorder="big") + + +class ArizePhoenixDataTrace(BaseTraceInstance): + def __init__( + self, + arize_phoenix_config: ArizeConfig | PhoenixConfig, + ): + super().__init__(arize_phoenix_config) + import logging + + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + self.arize_phoenix_config = arize_phoenix_config + self.tracer, self.processor = setup_tracer(arize_phoenix_config) + self.project = arize_phoenix_config.project + self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001") + + def trace(self, trace_info: BaseTraceInfo): + logger.info(f"[Arize/Phoenix] Trace: {trace_info}") + try: + if isinstance(trace_info, WorkflowTraceInfo): + self.workflow_trace(trace_info) + if isinstance(trace_info, MessageTraceInfo): + self.message_trace(trace_info) + if isinstance(trace_info, ModerationTraceInfo): + self.moderation_trace(trace_info) + if isinstance(trace_info, SuggestedQuestionTraceInfo): + self.suggested_question_trace(trace_info) + if isinstance(trace_info, DatasetRetrievalTraceInfo): + self.dataset_retrieval_trace(trace_info) + if isinstance(trace_info, ToolTraceInfo): + self.tool_trace(trace_info) + if isinstance(trace_info, GenerateNameTraceInfo): + self.generate_name_trace(trace_info) + + except Exception as e: + logger.error(f"[Arize/Phoenix] Error in the trace: {str(e)}", exc_info=True) + raise + + def workflow_trace(self, trace_info: WorkflowTraceInfo): + if trace_info.message_data is None: + return + + workflow_metadata = { + "workflow_id": trace_info.workflow_run_id or "", + "message_id": trace_info.message_id or "", + "workflow_app_log_id": trace_info.workflow_app_log_id or "", + "status": trace_info.workflow_run_status or "", + "status_message": trace_info.error or "", + "level": "ERROR" if trace_info.error else "DEFAULT", + "total_tokens": trace_info.total_tokens or 0, + } + workflow_metadata.update(trace_info.metadata) + + trace_id = uuid_to_trace_id(trace_info.message_id) + span_id = RandomIdGenerator().generate_span_id() + context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + workflow_span = self.tracer.start_span( + name=TraceTaskName.WORKFLOW_TRACE.value, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.workflow_run_inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False), + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.CHAIN.value, + SpanAttributes.METADATA: json.dumps(workflow_metadata, ensure_ascii=False), + SpanAttributes.SESSION_ID: trace_info.conversation_id or "", + }, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(context)), + ) + + try: + # Process workflow nodes + for node_execution in self._get_workflow_nodes(trace_info.workflow_run_id): + created_at = node_execution.created_at or datetime.now() + elapsed_time = node_execution.elapsed_time + finished_at = created_at + timedelta(seconds=elapsed_time) + + process_data = json.loads(node_execution.process_data) if node_execution.process_data else {} + + node_metadata = { + "node_id": node_execution.id, + "node_type": node_execution.node_type, + "node_status": node_execution.status, + "tenant_id": node_execution.tenant_id, + "app_id": node_execution.app_id, + "app_name": node_execution.title, + "status": node_execution.status, + "level": "ERROR" if node_execution.status != "succeeded" else "DEFAULT", + } + + if node_execution.execution_metadata: + node_metadata.update(json.loads(node_execution.execution_metadata)) + + # Determine the correct span kind based on node type + span_kind = OpenInferenceSpanKindValues.CHAIN.value + if node_execution.node_type == "llm": + span_kind = OpenInferenceSpanKindValues.LLM.value + provider = process_data.get("model_provider") + model = process_data.get("model_name") + if provider: + node_metadata["ls_provider"] = provider + if model: + node_metadata["ls_model_name"] = model + + outputs = json.loads(node_execution.outputs).get("usage", {}) + usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + if usage_data: + node_metadata["total_tokens"] = usage_data.get("total_tokens", 0) + node_metadata["prompt_tokens"] = usage_data.get("prompt_tokens", 0) + node_metadata["completion_tokens"] = usage_data.get("completion_tokens", 0) + elif node_execution.node_type == "dataset_retrieval": + span_kind = OpenInferenceSpanKindValues.RETRIEVER.value + elif node_execution.node_type == "tool": + span_kind = OpenInferenceSpanKindValues.TOOL.value + else: + span_kind = OpenInferenceSpanKindValues.CHAIN.value + + node_span = self.tracer.start_span( + name=node_execution.node_type, + attributes={ + SpanAttributes.INPUT_VALUE: node_execution.inputs or "{}", + SpanAttributes.OUTPUT_VALUE: node_execution.outputs or "{}", + SpanAttributes.OPENINFERENCE_SPAN_KIND: span_kind, + SpanAttributes.METADATA: json.dumps(node_metadata, ensure_ascii=False), + SpanAttributes.SESSION_ID: trace_info.conversation_id or "", + }, + start_time=datetime_to_nanos(created_at), + ) + + try: + if node_execution.node_type == "llm": + provider = process_data.get("model_provider") + model = process_data.get("model_name") + if provider: + node_span.set_attribute(SpanAttributes.LLM_PROVIDER, provider) + if model: + node_span.set_attribute(SpanAttributes.LLM_MODEL_NAME, model) + + outputs = json.loads(node_execution.outputs).get("usage", {}) + usage_data = ( + process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + ) + if usage_data: + node_span.set_attribute( + SpanAttributes.LLM_TOKEN_COUNT_TOTAL, usage_data.get("total_tokens", 0) + ) + node_span.set_attribute( + SpanAttributes.LLM_TOKEN_COUNT_PROMPT, usage_data.get("prompt_tokens", 0) + ) + node_span.set_attribute( + SpanAttributes.LLM_TOKEN_COUNT_COMPLETION, usage_data.get("completion_tokens", 0) + ) + finally: + node_span.end(end_time=datetime_to_nanos(finished_at)) + finally: + workflow_span.end(end_time=datetime_to_nanos(trace_info.end_time)) + + def message_trace(self, trace_info: MessageTraceInfo): + if trace_info.message_data is None: + return + + file_list = cast(list[str], trace_info.file_list) or [] + message_file_data: Optional[MessageFile] = trace_info.message_file_data + + if message_file_data is not None: + file_url = f"{self.file_base_url}/{message_file_data.url}" if message_file_data else "" + file_list.append(file_url) + + message_metadata = { + "message_id": trace_info.message_id or "", + "conversation_mode": str(trace_info.conversation_mode or ""), + "user_id": trace_info.message_data.from_account_id or "", + "file_list": json.dumps(file_list), + "status": trace_info.message_data.status or "", + "status_message": trace_info.error or "", + "level": "ERROR" if trace_info.error else "DEFAULT", + "total_tokens": trace_info.total_tokens or 0, + "prompt_tokens": trace_info.message_tokens or 0, + "completion_tokens": trace_info.answer_tokens or 0, + "ls_provider": trace_info.message_data.model_provider or "", + "ls_model_name": trace_info.message_data.model_id or "", + } + message_metadata.update(trace_info.metadata) + + # Add end user data if available + if trace_info.message_data.from_end_user_id: + end_user_data: Optional[EndUser] = ( + db.session.query(EndUser).filter(EndUser.id == trace_info.message_data.from_end_user_id).first() + ) + if end_user_data is not None: + message_metadata["end_user_id"] = end_user_data.session_id + + attributes = { + SpanAttributes.INPUT_VALUE: trace_info.message_data.query, + SpanAttributes.OUTPUT_VALUE: trace_info.message_data.answer, + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.CHAIN.value, + SpanAttributes.METADATA: json.dumps(message_metadata, ensure_ascii=False), + SpanAttributes.SESSION_ID: trace_info.message_data.conversation_id, + } + + trace_id = uuid_to_trace_id(trace_info.message_id) + message_span_id = RandomIdGenerator().generate_span_id() + span_context = SpanContext( + trace_id=trace_id, + span_id=message_span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + message_span = self.tracer.start_span( + name=TraceTaskName.MESSAGE_TRACE.value, + attributes=attributes, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(span_context)), + ) + + try: + if trace_info.error: + message_span.add_event( + "exception", + attributes={ + "exception.message": trace_info.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.error, + }, + ) + + # Convert outputs to string based on type + if isinstance(trace_info.outputs, dict | list): + outputs_str = json.dumps(trace_info.outputs, ensure_ascii=False) + elif isinstance(trace_info.outputs, str): + outputs_str = trace_info.outputs + else: + outputs_str = str(trace_info.outputs) + + llm_attributes = { + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.LLM.value, + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: outputs_str, + SpanAttributes.METADATA: json.dumps(message_metadata, ensure_ascii=False), + SpanAttributes.SESSION_ID: trace_info.message_data.conversation_id, + } + + if isinstance(trace_info.inputs, list): + for i, msg in enumerate(trace_info.inputs): + if isinstance(msg, dict): + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.{i}.message.content"] = msg.get("text", "") + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.{i}.message.role"] = msg.get( + "role", "user" + ) + # todo: handle assistant and tool role messages, as they don't always + # have a text field, but may have a tool_calls field instead + # e.g. 'tool_calls': [{'id': '98af3a29-b066-45a5-b4b1-46c74ddafc58', + # 'type': 'function', 'function': {'name': 'current_time', 'arguments': '{}'}}]} + elif isinstance(trace_info.inputs, dict): + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.0.message.content"] = json.dumps(trace_info.inputs) + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.0.message.role"] = "user" + elif isinstance(trace_info.inputs, str): + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.0.message.content"] = trace_info.inputs + llm_attributes[f"{SpanAttributes.LLM_INPUT_MESSAGES}.0.message.role"] = "user" + + if trace_info.total_tokens is not None and trace_info.total_tokens > 0: + llm_attributes[SpanAttributes.LLM_TOKEN_COUNT_TOTAL] = trace_info.total_tokens + if trace_info.message_tokens is not None and trace_info.message_tokens > 0: + llm_attributes[SpanAttributes.LLM_TOKEN_COUNT_PROMPT] = trace_info.message_tokens + if trace_info.answer_tokens is not None and trace_info.answer_tokens > 0: + llm_attributes[SpanAttributes.LLM_TOKEN_COUNT_COMPLETION] = trace_info.answer_tokens + + if trace_info.message_data.model_id is not None: + llm_attributes[SpanAttributes.LLM_MODEL_NAME] = trace_info.message_data.model_id + if trace_info.message_data.model_provider is not None: + llm_attributes[SpanAttributes.LLM_PROVIDER] = trace_info.message_data.model_provider + + if trace_info.message_data and trace_info.message_data.message_metadata: + metadata_dict = json.loads(trace_info.message_data.message_metadata) + if model_params := metadata_dict.get("model_parameters"): + llm_attributes[SpanAttributes.LLM_INVOCATION_PARAMETERS] = json.dumps(model_params) + + llm_span = self.tracer.start_span( + name="llm", + attributes=llm_attributes, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(span_context)), + ) + + try: + if trace_info.error: + llm_span.add_event( + "exception", + attributes={ + "exception.message": trace_info.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.error, + }, + ) + finally: + llm_span.end(end_time=datetime_to_nanos(trace_info.end_time)) + finally: + message_span.end(end_time=datetime_to_nanos(trace_info.end_time)) + + def moderation_trace(self, trace_info: ModerationTraceInfo): + if trace_info.message_data is None: + return + + metadata = { + "message_id": trace_info.message_id, + "tool_name": "moderation", + "status": trace_info.message_data.status, + "status_message": trace_info.message_data.error or "", + "level": "ERROR" if trace_info.message_data.error else "DEFAULT", + } + metadata.update(trace_info.metadata) + + trace_id = uuid_to_trace_id(trace_info.message_id) + span_id = RandomIdGenerator().generate_span_id() + context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + span = self.tracer.start_span( + name=TraceTaskName.MODERATION_TRACE.value, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: json.dumps( + { + "action": trace_info.action, + "flagged": trace_info.flagged, + "preset_response": trace_info.preset_response, + "inputs": trace_info.inputs, + }, + ensure_ascii=False, + ), + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.CHAIN.value, + SpanAttributes.METADATA: json.dumps(metadata, ensure_ascii=False), + }, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(context)), + ) + + try: + if trace_info.message_data.error: + span.add_event( + "exception", + attributes={ + "exception.message": trace_info.message_data.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.message_data.error, + }, + ) + finally: + span.end(end_time=datetime_to_nanos(trace_info.end_time)) + + def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo): + if trace_info.message_data is None: + return + + start_time = trace_info.start_time or trace_info.message_data.created_at + end_time = trace_info.end_time or trace_info.message_data.updated_at + + metadata = { + "message_id": trace_info.message_id, + "tool_name": "suggested_question", + "status": trace_info.status, + "status_message": trace_info.error or "", + "level": "ERROR" if trace_info.error else "DEFAULT", + "total_tokens": trace_info.total_tokens, + "ls_provider": trace_info.model_provider or "", + "ls_model_name": trace_info.model_id or "", + } + metadata.update(trace_info.metadata) + + trace_id = uuid_to_trace_id(trace_info.message_id) + span_id = RandomIdGenerator().generate_span_id() + context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + span = self.tracer.start_span( + name=TraceTaskName.SUGGESTED_QUESTION_TRACE.value, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: json.dumps(trace_info.suggested_question, ensure_ascii=False), + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.CHAIN.value, + SpanAttributes.METADATA: json.dumps(metadata, ensure_ascii=False), + }, + start_time=datetime_to_nanos(start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(context)), + ) + + try: + if trace_info.error: + span.add_event( + "exception", + attributes={ + "exception.message": trace_info.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.error, + }, + ) + finally: + span.end(end_time=datetime_to_nanos(end_time)) + + def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo): + if trace_info.message_data is None: + return + + start_time = trace_info.start_time or trace_info.message_data.created_at + end_time = trace_info.end_time or trace_info.message_data.updated_at + + metadata = { + "message_id": trace_info.message_id, + "tool_name": "dataset_retrieval", + "status": trace_info.message_data.status, + "status_message": trace_info.message_data.error or "", + "level": "ERROR" if trace_info.message_data.error else "DEFAULT", + "ls_provider": trace_info.message_data.model_provider or "", + "ls_model_name": trace_info.message_data.model_id or "", + } + metadata.update(trace_info.metadata) + + trace_id = uuid_to_trace_id(trace_info.message_id) + span_id = RandomIdGenerator().generate_span_id() + context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + span = self.tracer.start_span( + name=TraceTaskName.DATASET_RETRIEVAL_TRACE.value, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: json.dumps({"documents": trace_info.documents}, ensure_ascii=False), + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.RETRIEVER.value, + SpanAttributes.METADATA: json.dumps(metadata, ensure_ascii=False), + "start_time": start_time.isoformat() if start_time else "", + "end_time": end_time.isoformat() if end_time else "", + }, + start_time=datetime_to_nanos(start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(context)), + ) + + try: + if trace_info.message_data.error: + span.add_event( + "exception", + attributes={ + "exception.message": trace_info.message_data.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.message_data.error, + }, + ) + finally: + span.end(end_time=datetime_to_nanos(end_time)) + + def tool_trace(self, trace_info: ToolTraceInfo): + if trace_info.message_data is None: + logger.warning("[Arize/Phoenix] Message data is None, skipping tool trace.") + return + + metadata = { + "message_id": trace_info.message_id, + "tool_config": json.dumps(trace_info.tool_config, ensure_ascii=False), + } + + trace_id = uuid_to_trace_id(trace_info.message_id) + tool_span_id = RandomIdGenerator().generate_span_id() + logger.info(f"[Arize/Phoenix] Creating tool trace with trace_id: {trace_id}, span_id: {tool_span_id}") + + # Create span context with the same trace_id as the parent + # todo: Create with the appropriate parent span context, so that the tool span is + # a child of the appropriate span (e.g. message span) + span_context = SpanContext( + trace_id=trace_id, + span_id=tool_span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + tool_params_str = ( + json.dumps(trace_info.tool_parameters, ensure_ascii=False) + if isinstance(trace_info.tool_parameters, dict) + else str(trace_info.tool_parameters) + ) + + span = self.tracer.start_span( + name=trace_info.tool_name, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.tool_inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: trace_info.tool_outputs, + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.TOOL.value, + SpanAttributes.METADATA: json.dumps(metadata, ensure_ascii=False), + SpanAttributes.TOOL_NAME: trace_info.tool_name, + SpanAttributes.TOOL_PARAMETERS: tool_params_str, + }, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(span_context)), + ) + + try: + if trace_info.error: + span.add_event( + "exception", + attributes={ + "exception.message": trace_info.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.error, + }, + ) + finally: + span.end(end_time=datetime_to_nanos(trace_info.end_time)) + + def generate_name_trace(self, trace_info: GenerateNameTraceInfo): + if trace_info.message_data is None: + return + + metadata = { + "project_name": self.project, + "message_id": trace_info.message_id, + "status": trace_info.message_data.status, + "status_message": trace_info.message_data.error or "", + "level": "ERROR" if trace_info.message_data.error else "DEFAULT", + } + metadata.update(trace_info.metadata) + + trace_id = uuid_to_trace_id(trace_info.message_id) + span_id = RandomIdGenerator().generate_span_id() + context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + trace_state=TraceState(), + ) + + span = self.tracer.start_span( + name=TraceTaskName.GENERATE_NAME_TRACE.value, + attributes={ + SpanAttributes.INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False), + SpanAttributes.OUTPUT_VALUE: json.dumps(trace_info.outputs, ensure_ascii=False), + SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.CHAIN.value, + SpanAttributes.METADATA: json.dumps(metadata, ensure_ascii=False), + SpanAttributes.SESSION_ID: trace_info.message_data.conversation_id, + "start_time": trace_info.start_time.isoformat() if trace_info.start_time else "", + "end_time": trace_info.end_time.isoformat() if trace_info.end_time else "", + }, + start_time=datetime_to_nanos(trace_info.start_time), + context=trace.set_span_in_context(trace.NonRecordingSpan(context)), + ) + + try: + if trace_info.message_data.error: + span.add_event( + "exception", + attributes={ + "exception.message": trace_info.message_data.error, + "exception.type": "Error", + "exception.stacktrace": trace_info.message_data.error, + }, + ) + finally: + span.end(end_time=datetime_to_nanos(trace_info.end_time)) + + def api_check(self): + try: + with self.tracer.start_span("api_check") as span: + span.set_attribute("test", "true") + return True + except Exception as e: + logger.info(f"[Arize/Phoenix] API check failed: {str(e)}", exc_info=True) + raise ValueError(f"[Arize/Phoenix] API check failed: {str(e)}") + + def get_project_url(self): + try: + if self.arize_phoenix_config.endpoint == "https://otlp.arize.com": + return "https://app.arize.com/" + else: + return f"{self.arize_phoenix_config.endpoint}/projects/" + except Exception as e: + logger.info(f"[Arize/Phoenix] Get run url failed: {str(e)}", exc_info=True) + raise ValueError(f"[Arize/Phoenix] Get run url failed: {str(e)}") + + def _get_workflow_nodes(self, workflow_run_id: str): + """Helper method to get workflow nodes""" + workflow_nodes = ( + db.session.query( + WorkflowNodeExecutionModel.id, + WorkflowNodeExecutionModel.tenant_id, + WorkflowNodeExecutionModel.app_id, + WorkflowNodeExecutionModel.title, + WorkflowNodeExecutionModel.node_type, + WorkflowNodeExecutionModel.status, + WorkflowNodeExecutionModel.inputs, + WorkflowNodeExecutionModel.outputs, + WorkflowNodeExecutionModel.created_at, + WorkflowNodeExecutionModel.elapsed_time, + WorkflowNodeExecutionModel.process_data, + WorkflowNodeExecutionModel.execution_metadata, + ) + .filter(WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id) + .all() + ) + return workflow_nodes diff --git a/api/core/ops/entities/config_entity.py b/api/core/ops/entities/config_entity.py index f2d1bd305a..89ff0cfded 100644 --- a/api/core/ops/entities/config_entity.py +++ b/api/core/ops/entities/config_entity.py @@ -2,20 +2,92 @@ from enum import StrEnum from pydantic import BaseModel, ValidationInfo, field_validator +from core.ops.utils import validate_project_name, validate_url, validate_url_with_path + class TracingProviderEnum(StrEnum): + ARIZE = "arize" + PHOENIX = "phoenix" LANGFUSE = "langfuse" LANGSMITH = "langsmith" OPIK = "opik" WEAVE = "weave" + ALIYUN = "aliyun" class BaseTracingConfig(BaseModel): """ - Base model class for tracing + Base model class for tracing configurations """ - ... + @classmethod + def validate_endpoint_url(cls, v: str, default_url: str) -> str: + """ + Common endpoint URL validation logic + + Args: + v: URL value to validate + default_url: Default URL to use if input is None or empty + + Returns: + Validated and normalized URL + """ + return validate_url(v, default_url) + + @classmethod + def validate_project_field(cls, v: str, default_name: str) -> str: + """ + Common project name validation logic + + Args: + v: Project name to validate + default_name: Default name to use if input is None or empty + + Returns: + Validated project name + """ + return validate_project_name(v, default_name) + + +class ArizeConfig(BaseTracingConfig): + """ + Model class for Arize tracing config. + """ + + api_key: str | None = None + space_id: str | None = None + project: str | None = None + endpoint: str = "https://otlp.arize.com" + + @field_validator("project") + @classmethod + def project_validator(cls, v, info: ValidationInfo): + return cls.validate_project_field(v, "default") + + @field_validator("endpoint") + @classmethod + def endpoint_validator(cls, v, info: ValidationInfo): + return cls.validate_endpoint_url(v, "https://otlp.arize.com") + + +class PhoenixConfig(BaseTracingConfig): + """ + Model class for Phoenix tracing config. + """ + + api_key: str | None = None + project: str | None = None + endpoint: str = "https://app.phoenix.arize.com" + + @field_validator("project") + @classmethod + def project_validator(cls, v, info: ValidationInfo): + return cls.validate_project_field(v, "default") + + @field_validator("endpoint") + @classmethod + def endpoint_validator(cls, v, info: ValidationInfo): + return cls.validate_endpoint_url(v, "https://app.phoenix.arize.com") class LangfuseConfig(BaseTracingConfig): @@ -29,13 +101,8 @@ class LangfuseConfig(BaseTracingConfig): @field_validator("host") @classmethod - def set_value(cls, v, info: ValidationInfo): - if v is None or v == "": - v = "https://api.langfuse.com" - if not v.startswith("https://") and not v.startswith("http://"): - raise ValueError("host must start with https:// or http://") - - return v + def host_validator(cls, v, info: ValidationInfo): + return cls.validate_endpoint_url(v, "https://api.langfuse.com") class LangSmithConfig(BaseTracingConfig): @@ -49,13 +116,9 @@ class LangSmithConfig(BaseTracingConfig): @field_validator("endpoint") @classmethod - def set_value(cls, v, info: ValidationInfo): - if v is None or v == "": - v = "https://api.smith.langchain.com" - if not v.startswith("https://"): - raise ValueError("endpoint must start with https://") - - return v + def endpoint_validator(cls, v, info: ValidationInfo): + # LangSmith only allows HTTPS + return validate_url(v, "https://api.smith.langchain.com", allowed_schemes=("https",)) class OpikConfig(BaseTracingConfig): @@ -71,22 +134,12 @@ class OpikConfig(BaseTracingConfig): @field_validator("project") @classmethod def project_validator(cls, v, info: ValidationInfo): - if v is None or v == "": - v = "Default Project" - - return v + return cls.validate_project_field(v, "Default Project") @field_validator("url") @classmethod def url_validator(cls, v, info: ValidationInfo): - if v is None or v == "": - v = "https://www.comet.com/opik/api/" - if not v.startswith(("https://", "http://")): - raise ValueError("url must start with https:// or http://") - if not v.endswith("/api/"): - raise ValueError("url should ends with /api/") - - return v + return validate_url_with_path(v, "https://www.comet.com/opik/api/", required_suffix="/api/") class WeaveConfig(BaseTracingConfig): @@ -98,17 +151,48 @@ class WeaveConfig(BaseTracingConfig): entity: str | None = None project: str endpoint: str = "https://trace.wandb.ai" + host: str | None = None @field_validator("endpoint") @classmethod - def set_value(cls, v, info: ValidationInfo): - if v is None or v == "": - v = "https://trace.wandb.ai" - if not v.startswith("https://"): - raise ValueError("endpoint must start with https://") + def endpoint_validator(cls, v, info: ValidationInfo): + # Weave only allows HTTPS for endpoint + return validate_url(v, "https://trace.wandb.ai", allowed_schemes=("https",)) + @field_validator("host") + @classmethod + def host_validator(cls, v, info: ValidationInfo): + if v is not None and v.strip() != "": + return validate_url(v, v, allowed_schemes=("https", "http")) + return v + + +class AliyunConfig(BaseTracingConfig): + """ + Model class for Aliyun tracing config. + """ + + app_name: str = "dify_app" + license_key: str + endpoint: str + + @field_validator("app_name") + @classmethod + def app_name_validator(cls, v, info: ValidationInfo): + return cls.validate_project_field(v, "dify_app") + + @field_validator("license_key") + @classmethod + def license_key_validator(cls, v, info: ValidationInfo): + if not v or v.strip() == "": + raise ValueError("License key cannot be empty") return v + @field_validator("endpoint") + @classmethod + def endpoint_validator(cls, v, info: ValidationInfo): + return cls.validate_endpoint_url(v, "https://tracing-analysis-dc-hz.aliyuncs.com") + OPS_FILE_PATH = "ops_trace/" OPS_TRACE_FAILED_KEY = "FAILED_OPS_TRACE" diff --git a/api/core/ops/langfuse_trace/langfuse_trace.py b/api/core/ops/langfuse_trace/langfuse_trace.py index 0ea74e9ef0..4a7e66d27c 100644 --- a/api/core/ops/langfuse_trace/langfuse_trace.py +++ b/api/core/ops/langfuse_trace/langfuse_trace.py @@ -28,10 +28,11 @@ from core.ops.langfuse_trace.entities.langfuse_trace_entity import ( UnitEnum, ) from core.ops.utils import filter_none_values -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.repositories import DifyCoreRepositoryFactory from core.workflow.nodes.enums import NodeType from extensions.ext_database import db from models import EndUser, WorkflowNodeExecutionTriggeredFrom +from models.enums import MessageStatus logger = logging.getLogger(__name__) @@ -83,6 +84,7 @@ class LangFuseDataTrace(BaseTraceInstance): metadata=metadata, session_id=trace_info.conversation_id, tags=["message", "workflow"], + version=trace_info.workflow_run_version, ) self.add_trace(langfuse_trace_data=trace_data) workflow_span_data = LangfuseSpan( @@ -108,6 +110,7 @@ class LangFuseDataTrace(BaseTraceInstance): metadata=metadata, session_id=trace_info.conversation_id, tags=["workflow"], + version=trace_info.workflow_run_version, ) self.add_trace(langfuse_trace_data=trace_data) @@ -120,10 +123,10 @@ class LangFuseDataTrace(BaseTraceInstance): service_account = self.get_service_account_with_tenant(app_id) - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=service_account, - app_id=trace_info.metadata.get("app_id"), + app_id=app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, ) @@ -172,48 +175,15 @@ class LangFuseDataTrace(BaseTraceInstance): } ) - # add span - if trace_info.message_id: - span_data = LangfuseSpan( - id=node_execution_id, - name=node_type, - input=inputs, - output=outputs, - trace_id=trace_id, - start_time=created_at, - end_time=finished_at, - metadata=metadata, - level=(LevelEnum.DEFAULT if status == "succeeded" else LevelEnum.ERROR), - status_message=trace_info.error or "", - parent_observation_id=trace_info.workflow_run_id, - ) - else: - span_data = LangfuseSpan( - id=node_execution_id, - name=node_type, - input=inputs, - output=outputs, - trace_id=trace_id, - start_time=created_at, - end_time=finished_at, - metadata=metadata, - level=(LevelEnum.DEFAULT if status == "succeeded" else LevelEnum.ERROR), - status_message=trace_info.error or "", - ) - - self.add_span(langfuse_span_data=span_data) - + # add generation span if process_data and process_data.get("model_mode") == "chat": total_token = metadata.get("total_tokens", 0) prompt_tokens = 0 completion_tokens = 0 try: - if outputs.get("usage"): - prompt_tokens = outputs.get("usage", {}).get("prompt_tokens", 0) - completion_tokens = outputs.get("usage", {}).get("completion_tokens", 0) - else: - prompt_tokens = process_data.get("usage", {}).get("prompt_tokens", 0) - completion_tokens = process_data.get("usage", {}).get("completion_tokens", 0) + usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + prompt_tokens = usage_data.get("prompt_tokens", 0) + completion_tokens = usage_data.get("completion_tokens", 0) except Exception: logger.error("Failed to extract usage", exc_info=True) @@ -226,10 +196,10 @@ class LangFuseDataTrace(BaseTraceInstance): ) node_generation_data = LangfuseGeneration( - name="llm", + id=node_execution_id, + name=node_name, trace_id=trace_id, model=process_data.get("model_name"), - parent_observation_id=node_execution_id, start_time=created_at, end_time=finished_at, input=inputs, @@ -237,11 +207,30 @@ class LangFuseDataTrace(BaseTraceInstance): metadata=metadata, level=(LevelEnum.DEFAULT if status == "succeeded" else LevelEnum.ERROR), status_message=trace_info.error or "", + parent_observation_id=trace_info.workflow_run_id if trace_info.message_id else None, usage=generation_usage, ) self.add_generation(langfuse_generation_data=node_generation_data) + # add normal span + else: + span_data = LangfuseSpan( + id=node_execution_id, + name=node_name, + input=inputs, + output=outputs, + trace_id=trace_id, + start_time=created_at, + end_time=finished_at, + metadata=metadata, + level=(LevelEnum.DEFAULT if status == "succeeded" else LevelEnum.ERROR), + status_message=trace_info.error or "", + parent_observation_id=trace_info.workflow_run_id if trace_info.message_id else None, + ) + + self.add_span(langfuse_span_data=span_data) + def message_trace(self, trace_info: MessageTraceInfo, **kwargs): # get message file data file_list = trace_info.file_list @@ -284,7 +273,7 @@ class LangFuseDataTrace(BaseTraceInstance): ) self.add_trace(langfuse_trace_data=trace_data) - # start add span + # add generation generation_usage = GenerationUsage( input=trace_info.message_tokens, output=trace_info.answer_tokens, @@ -302,7 +291,7 @@ class LangFuseDataTrace(BaseTraceInstance): input=trace_info.inputs, output=message_data.answer, metadata=metadata, - level=(LevelEnum.DEFAULT if message_data.status != "error" else LevelEnum.ERROR), + level=(LevelEnum.DEFAULT if message_data.status != MessageStatus.ERROR else LevelEnum.ERROR), status_message=message_data.error or "", usage=generation_usage, ) @@ -348,7 +337,7 @@ class LangFuseDataTrace(BaseTraceInstance): start_time=trace_info.start_time, end_time=trace_info.end_time, metadata=trace_info.metadata, - level=(LevelEnum.DEFAULT if message_data.status != "error" else LevelEnum.ERROR), + level=(LevelEnum.DEFAULT if message_data.status != MessageStatus.ERROR else LevelEnum.ERROR), status_message=message_data.error or "", usage=generation_usage, ) diff --git a/api/core/ops/langsmith_trace/langsmith_trace.py b/api/core/ops/langsmith_trace/langsmith_trace.py index 8a392940db..8a559c4929 100644 --- a/api/core/ops/langsmith_trace/langsmith_trace.py +++ b/api/core/ops/langsmith_trace/langsmith_trace.py @@ -27,7 +27,7 @@ from core.ops.langsmith_trace.entities.langsmith_trace_entity import ( LangSmithRunUpdateModel, ) from core.ops.utils import filter_none_values, generate_dotted_order -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.repositories import DifyCoreRepositoryFactory from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.nodes.enums import NodeType from extensions.ext_database import db @@ -145,10 +145,10 @@ class LangSmithDataTrace(BaseTraceInstance): service_account = self.get_service_account_with_tenant(app_id) - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=service_account, - app_id=trace_info.metadata.get("app_id"), + app_id=app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, ) @@ -206,12 +206,9 @@ class LangSmithDataTrace(BaseTraceInstance): prompt_tokens = 0 completion_tokens = 0 try: - if outputs.get("usage"): - prompt_tokens = outputs.get("usage", {}).get("prompt_tokens", 0) - completion_tokens = outputs.get("usage", {}).get("completion_tokens", 0) - else: - prompt_tokens = process_data.get("usage", {}).get("prompt_tokens", 0) - completion_tokens = process_data.get("usage", {}).get("completion_tokens", 0) + usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + prompt_tokens = usage_data.get("prompt_tokens", 0) + completion_tokens = usage_data.get("completion_tokens", 0) except Exception: logger.error("Failed to extract usage", exc_info=True) diff --git a/api/core/ops/opik_trace/opik_trace.py b/api/core/ops/opik_trace/opik_trace.py index f4d2760ba5..be4997a5bf 100644 --- a/api/core/ops/opik_trace/opik_trace.py +++ b/api/core/ops/opik_trace/opik_trace.py @@ -21,7 +21,7 @@ from core.ops.entities.trace_entity import ( TraceTaskName, WorkflowTraceInfo, ) -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.repositories import DifyCoreRepositoryFactory from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.nodes.enums import NodeType from extensions.ext_database import db @@ -160,10 +160,10 @@ class OpikDataTrace(BaseTraceInstance): service_account = self.get_service_account_with_tenant(app_id) - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=service_account, - app_id=trace_info.metadata.get("app_id"), + app_id=app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, ) @@ -222,10 +222,10 @@ class OpikDataTrace(BaseTraceInstance): ) try: - if outputs.get("usage"): - total_tokens = outputs["usage"].get("total_tokens", 0) - prompt_tokens = outputs["usage"].get("prompt_tokens", 0) - completion_tokens = outputs["usage"].get("completion_tokens", 0) + usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {}) + total_tokens = usage_data.get("total_tokens", 0) + prompt_tokens = usage_data.get("prompt_tokens", 0) + completion_tokens = usage_data.get("completion_tokens", 0) except Exception: logger.error("Failed to extract usage", exc_info=True) @@ -241,7 +241,7 @@ class OpikDataTrace(BaseTraceInstance): "trace_id": opik_trace_id, "id": prepare_opik_uuid(created_at, node_execution_id), "parent_span_id": prepare_opik_uuid(trace_info.start_time, parent_span_id), - "name": node_type, + "name": node_name, "type": run_type, "start_time": created_at, "end_time": finished_at, diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index dc4cfc48db..5c9b9d27b7 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -81,9 +81,39 @@ class OpsTraceProviderConfigMap(dict[str, dict[str, Any]]): return { "config_class": WeaveConfig, "secret_keys": ["api_key"], - "other_keys": ["project", "entity", "endpoint"], + "other_keys": ["project", "entity", "endpoint", "host"], "trace_instance": WeaveDataTrace, } + case TracingProviderEnum.ARIZE: + from core.ops.arize_phoenix_trace.arize_phoenix_trace import ArizePhoenixDataTrace + from core.ops.entities.config_entity import ArizeConfig + + return { + "config_class": ArizeConfig, + "secret_keys": ["api_key", "space_id"], + "other_keys": ["project", "endpoint"], + "trace_instance": ArizePhoenixDataTrace, + } + case TracingProviderEnum.PHOENIX: + from core.ops.arize_phoenix_trace.arize_phoenix_trace import ArizePhoenixDataTrace + from core.ops.entities.config_entity import PhoenixConfig + + return { + "config_class": PhoenixConfig, + "secret_keys": ["api_key"], + "other_keys": ["project", "endpoint"], + "trace_instance": ArizePhoenixDataTrace, + } + case TracingProviderEnum.ALIYUN: + from core.ops.aliyun_trace.aliyun_trace import AliyunDataTrace + from core.ops.entities.config_entity import AliyunConfig + + return { + "config_class": AliyunConfig, + "secret_keys": ["license_key"], + "other_keys": ["endpoint", "app_name"], + "trace_instance": AliyunDataTrace, + } case _: raise KeyError(f"Unsupported tracing provider: {provider}") @@ -251,7 +281,7 @@ class OpsTraceManager: provider_config_map[tracing_provider]["trace_instance"], provider_config_map[tracing_provider]["config_class"], ) - decrypt_trace_config_key = str(decrypt_trace_config) + decrypt_trace_config_key = json.dumps(decrypt_trace_config, sort_keys=True) tracing_instance = cls.ops_trace_instances_cache.get(decrypt_trace_config_key) if tracing_instance is None: # create new tracing_instance and update the cache if it absent diff --git a/api/core/ops/utils.py b/api/core/ops/utils.py index 8b06df1930..36d060afd2 100644 --- a/api/core/ops/utils.py +++ b/api/core/ops/utils.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from datetime import datetime from typing import Optional, Union +from urllib.parse import urlparse from extensions.ext_database import db from models.model import Message @@ -60,3 +61,83 @@ def generate_dotted_order( return current_segment return f"{parent_dotted_order}.{current_segment}" + + +def validate_url(url: str, default_url: str, allowed_schemes: tuple = ("https", "http")) -> str: + """ + Validate and normalize URL with proper error handling + + Args: + url: The URL to validate + default_url: Default URL to use if input is None or empty + allowed_schemes: Tuple of allowed URL schemes (default: https, http) + + Returns: + Normalized URL string + + Raises: + ValueError: If URL format is invalid or scheme not allowed + """ + if not url or url.strip() == "": + return default_url + + # Parse URL to validate format + parsed = urlparse(url) + + # Check if scheme is allowed + if parsed.scheme not in allowed_schemes: + raise ValueError(f"URL scheme must be one of: {', '.join(allowed_schemes)}") + + # Reconstruct URL with only scheme, netloc (removing path, query, fragment) + normalized_url = f"{parsed.scheme}://{parsed.netloc}" + + return normalized_url + + +def validate_url_with_path(url: str, default_url: str, required_suffix: str | None = None) -> str: + """ + Validate URL that may include path components + + Args: + url: The URL to validate + default_url: Default URL to use if input is None or empty + required_suffix: Optional suffix that URL must end with + + Returns: + Validated URL string + + Raises: + ValueError: If URL format is invalid or doesn't match required suffix + """ + if not url or url.strip() == "": + return default_url + + # Parse URL to validate format + parsed = urlparse(url) + + # Check if scheme is allowed + if parsed.scheme not in ("https", "http"): + raise ValueError("URL must start with https:// or http://") + + # Check required suffix if specified + if required_suffix and not url.endswith(required_suffix): + raise ValueError(f"URL should end with {required_suffix}") + + return url + + +def validate_project_name(project: str, default_name: str) -> str: + """ + Validate and normalize project name + + Args: + project: Project name to validate + default_name: Default name to use if input is None or empty + + Returns: + Normalized project name + """ + if not project or project.strip() == "": + return default_name + + return project.strip() diff --git a/api/core/ops/weave_trace/weave_trace.py b/api/core/ops/weave_trace/weave_trace.py index cfc8a505bb..445c6a8741 100644 --- a/api/core/ops/weave_trace/weave_trace.py +++ b/api/core/ops/weave_trace/weave_trace.py @@ -22,7 +22,7 @@ from core.ops.entities.trace_entity import ( WorkflowTraceInfo, ) from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.repositories import DifyCoreRepositoryFactory from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey from core.workflow.nodes.enums import NodeType from extensions.ext_database import db @@ -40,9 +40,14 @@ class WeaveDataTrace(BaseTraceInstance): self.weave_api_key = weave_config.api_key self.project_name = weave_config.project self.entity = weave_config.entity + self.host = weave_config.host + + # Login with API key first, including host if provided + if self.host: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host) + else: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) - # Login with API key first - login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) if not login_status: logger.error("Failed to login to Weights & Biases with the provided API key") raise ValueError("Weave login failed") @@ -139,10 +144,10 @@ class WeaveDataTrace(BaseTraceInstance): service_account = self.get_service_account_with_tenant(app_id) - workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository( + workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=session_factory, user=service_account, - app_id=trace_info.metadata.get("app_id"), + app_id=app_id, triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, ) @@ -386,7 +391,11 @@ class WeaveDataTrace(BaseTraceInstance): def api_check(self): try: - login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) + if self.host: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host) + else: + login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True) + if not login_status: raise ValueError("Weave login failed") else: diff --git a/api/core/plugin/backwards_invocation/base.py b/api/core/plugin/backwards_invocation/base.py index 3214e07469..2a5f857576 100644 --- a/api/core/plugin/backwards_invocation/base.py +++ b/api/core/plugin/backwards_invocation/base.py @@ -11,14 +11,12 @@ class BaseBackwardsInvocation: try: for chunk in response: if isinstance(chunk, BaseModel | dict): - yield BaseBackwardsInvocationResponse(data=chunk).model_dump_json().encode() + b"\n\n" - elif isinstance(chunk, str): - yield f"event: {chunk}\n\n".encode() + yield BaseBackwardsInvocationResponse(data=chunk).model_dump_json().encode() except Exception as e: error_message = BaseBackwardsInvocationResponse(error=str(e)).model_dump_json() - yield f"{error_message}\n\n".encode() + yield error_message.encode() else: - yield BaseBackwardsInvocationResponse(data=response).model_dump_json().encode() + b"\n\n" + yield BaseBackwardsInvocationResponse(data=response).model_dump_json().encode() T = TypeVar("T", bound=dict | Mapping | str | bool | int | BaseModel) diff --git a/api/core/plugin/backwards_invocation/model.py b/api/core/plugin/backwards_invocation/model.py index 17cfaf2edf..d07ab3d0c4 100644 --- a/api/core/plugin/backwards_invocation/model.py +++ b/api/core/plugin/backwards_invocation/model.py @@ -2,8 +2,15 @@ import tempfile from binascii import hexlify, unhexlify from collections.abc import Generator +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from core.model_manager import ModelManager -from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta +from core.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultChunkWithStructuredOutput, + LLMResultWithStructuredOutput, +) from core.model_runtime.entities.message_entities import ( PromptMessage, SystemPromptMessage, @@ -12,6 +19,7 @@ from core.model_runtime.entities.message_entities import ( from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from core.plugin.entities.request import ( RequestInvokeLLM, + RequestInvokeLLMWithStructuredOutput, RequestInvokeModeration, RequestInvokeRerank, RequestInvokeSpeech2Text, @@ -21,7 +29,7 @@ from core.plugin.entities.request import ( ) from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils -from core.workflow.nodes.llm.node import LLMNode +from core.workflow.nodes.llm import llm_utils from models.account import Tenant @@ -55,7 +63,7 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation): def handle() -> Generator[LLMResultChunk, None, None]: for chunk in response: if chunk.delta.usage: - LLMNode.deduct_llm_quota( + llm_utils.deduct_llm_quota( tenant_id=tenant.id, model_instance=model_instance, usage=chunk.delta.usage ) chunk.prompt_messages = [] @@ -64,7 +72,7 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation): return handle() else: if response.usage: - LLMNode.deduct_llm_quota(tenant_id=tenant.id, model_instance=model_instance, usage=response.usage) + llm_utils.deduct_llm_quota(tenant_id=tenant.id, model_instance=model_instance, usage=response.usage) def handle_non_streaming(response: LLMResult) -> Generator[LLMResultChunk, None, None]: yield LLMResultChunk( @@ -81,6 +89,72 @@ class PluginModelBackwardsInvocation(BaseBackwardsInvocation): return handle_non_streaming(response) + @classmethod + def invoke_llm_with_structured_output( + cls, user_id: str, tenant: Tenant, payload: RequestInvokeLLMWithStructuredOutput + ): + """ + invoke llm with structured output + """ + model_instance = ModelManager().get_model_instance( + tenant_id=tenant.id, + provider=payload.provider, + model_type=payload.model_type, + model=payload.model, + ) + + model_schema = model_instance.model_type_instance.get_model_schema(payload.model, model_instance.credentials) + + if not model_schema: + raise ValueError(f"Model schema not found for {payload.model}") + + response = invoke_llm_with_structured_output( + provider=payload.provider, + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=payload.prompt_messages, + json_schema=payload.structured_output_schema, + tools=payload.tools, + stop=payload.stop, + stream=True if payload.stream is None else payload.stream, + user=user_id, + model_parameters=payload.completion_params, + ) + + if isinstance(response, Generator): + + def handle() -> Generator[LLMResultChunkWithStructuredOutput, None, None]: + for chunk in response: + if chunk.delta.usage: + llm_utils.deduct_llm_quota( + tenant_id=tenant.id, model_instance=model_instance, usage=chunk.delta.usage + ) + chunk.prompt_messages = [] + yield chunk + + return handle() + else: + if response.usage: + llm_utils.deduct_llm_quota(tenant_id=tenant.id, model_instance=model_instance, usage=response.usage) + + def handle_non_streaming( + response: LLMResultWithStructuredOutput, + ) -> Generator[LLMResultChunkWithStructuredOutput, None, None]: + yield LLMResultChunkWithStructuredOutput( + model=response.model, + prompt_messages=[], + system_fingerprint=response.system_fingerprint, + structured_output=response.structured_output, + delta=LLMResultChunkDelta( + index=0, + message=response.message, + usage=response.usage, + finish_reason="", + ), + ) + + return handle_non_streaming(response) + @classmethod def invoke_text_embedding(cls, user_id: str, tenant: Tenant, payload: RequestInvokeTextEmbedding): """ diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index 9c99d67c0c..47290ee613 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -11,6 +11,9 @@ from core.workflow.nodes.base.entities import NumberType class PluginParameterOption(BaseModel): value: str = Field(..., description="The value of the option") label: I18nObject = Field(..., description="The label of the option") + icon: Optional[str] = Field( + default=None, description="The icon of the option, can be a url or a base64 encoded image" + ) @field_validator("value", mode="before") @classmethod @@ -37,10 +40,24 @@ class PluginParameterType(enum.StrEnum): MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value ANY = CommonParameterType.ANY.value + DYNAMIC_SELECT = CommonParameterType.DYNAMIC_SELECT.value # deprecated, should not use. SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value + # MCP object and array type parameters + ARRAY = CommonParameterType.ARRAY.value + OBJECT = CommonParameterType.OBJECT.value + + +class MCPServerParameterType(enum.StrEnum): + """ + MCP server got complex parameter types + """ + + ARRAY = "array" + OBJECT = "object" + class PluginParameterAutoGenerate(BaseModel): class Type(enum.StrEnum): @@ -140,6 +157,34 @@ def cast_parameter_value(typ: enum.StrEnum, value: Any, /): if value and not isinstance(value, str | dict | list | NumberType): raise ValueError("The var selector must be a string, dictionary, list or number.") return value + case PluginParameterType.ARRAY: + if not isinstance(value, list): + # Try to parse JSON string for arrays + if isinstance(value, str): + try: + import json + + parsed_value = json.loads(value) + if isinstance(parsed_value, list): + return parsed_value + except (json.JSONDecodeError, ValueError): + pass + return [value] + return value + case PluginParameterType.OBJECT: + if not isinstance(value, dict): + # Try to parse JSON string for objects + if isinstance(value, str): + try: + import json + + parsed_value = json.loads(value) + if isinstance(parsed_value, dict): + return parsed_value + except (json.JSONDecodeError, ValueError): + pass + return {} + return value case _: return str(value) except ValueError: diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index bdf7d5ce1f..e5cf7ee03a 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -72,12 +72,14 @@ class PluginDeclaration(BaseModel): class Meta(BaseModel): minimum_dify_version: Optional[str] = Field(default=None, pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$") + version: Optional[str] = Field(default=None) version: str = Field(..., pattern=r"^\d{1,4}(\.\d{1,4}){1,3}(-\w{1,16})?$") author: Optional[str] = Field(..., pattern=r"^[a-zA-Z0-9_-]{1,64}$") name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") description: I18nObject icon: str + icon_dark: Optional[str] = Field(default=None) label: I18nObject category: PluginCategory created_at: datetime.datetime diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index e9275c31cc..00253b8a11 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -1,4 +1,4 @@ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from datetime import datetime from enum import StrEnum from typing import Any, Generic, Optional, TypeVar @@ -9,6 +9,7 @@ from core.agent.plugin_entities import AgentProviderEntityWithPlugin from core.model_runtime.entities.model_entities import AIModelEntity from core.model_runtime.entities.provider_entities import ProviderEntity from core.plugin.entities.base import BasePluginEntity +from core.plugin.entities.parameters import PluginParameterOption from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin @@ -52,6 +53,7 @@ class PluginAgentProviderEntity(BaseModel): plugin_unique_identifier: str plugin_id: str declaration: AgentProviderEntityWithPlugin + meta: PluginDeclaration.Meta class PluginBasicBooleanResponse(BaseModel): @@ -156,9 +158,23 @@ class PluginInstallTaskStartResponse(BaseModel): task_id: str = Field(description="The ID of the install task.") -class PluginUploadResponse(BaseModel): +class PluginVerification(BaseModel): + """ + Verification of the plugin. + """ + + class AuthorizedCategory(StrEnum): + Langgenius = "langgenius" + Partner = "partner" + Community = "community" + + authorized_category: AuthorizedCategory = Field(description="The authorized category of the plugin.") + + +class PluginDecodeResponse(BaseModel): unique_identifier: str = Field(description="The unique identifier of the plugin.") manifest: PluginDeclaration + verification: Optional[PluginVerification] = Field(default=None, description="Basic verification information") class PluginOAuthAuthorizationUrlResponse(BaseModel): @@ -172,3 +188,7 @@ class PluginOAuthCredentialsResponse(BaseModel): class PluginListResponse(BaseModel): list: list[PluginEntity] total: int + + +class PluginDynamicSelectOptionsResponse(BaseModel): + options: Sequence[PluginParameterOption] = Field(description="The options of the dynamic select.") diff --git a/api/core/plugin/entities/request.py b/api/core/plugin/entities/request.py index 1692020ec8..89f595ec46 100644 --- a/api/core/plugin/entities/request.py +++ b/api/core/plugin/entities/request.py @@ -32,7 +32,7 @@ class RequestInvokeTool(BaseModel): Request to invoke a tool """ - tool_type: Literal["builtin", "workflow", "api"] + tool_type: Literal["builtin", "workflow", "api", "mcp"] provider: str tool: str tool_parameters: dict @@ -82,6 +82,16 @@ class RequestInvokeLLM(BaseRequestInvokeModel): return v +class RequestInvokeLLMWithStructuredOutput(RequestInvokeLLM): + """ + Request to invoke LLM with structured output + """ + + structured_output_schema: dict[str, Any] = Field( + default_factory=dict, description="The schema of the structured output in JSON schema format" + ) + + class RequestInvokeTextEmbedding(BaseRequestInvokeModel): """ Request to invoke text embedding diff --git a/api/core/plugin/impl/dynamic_select.py b/api/core/plugin/impl/dynamic_select.py new file mode 100644 index 0000000000..004412afd7 --- /dev/null +++ b/api/core/plugin/impl/dynamic_select.py @@ -0,0 +1,45 @@ +from collections.abc import Mapping +from typing import Any + +from core.plugin.entities.plugin import GenericProviderID +from core.plugin.entities.plugin_daemon import PluginDynamicSelectOptionsResponse +from core.plugin.impl.base import BasePluginClient + + +class DynamicSelectClient(BasePluginClient): + def fetch_dynamic_select_options( + self, + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + action: str, + credentials: Mapping[str, Any], + parameter: str, + ) -> PluginDynamicSelectOptionsResponse: + """ + Fetch dynamic select options for a plugin parameter. + """ + response = self._request_with_plugin_daemon_response_stream( + "POST", + f"plugin/{tenant_id}/dispatch/dynamic_select/fetch_parameter_options", + PluginDynamicSelectOptionsResponse, + data={ + "user_id": user_id, + "data": { + "provider": GenericProviderID(provider).provider_name, + "credentials": credentials, + "provider_action": action, + "parameter": parameter, + }, + }, + headers={ + "X-Plugin-ID": plugin_id, + "Content-Type": "application/json", + }, + ) + + for options in response: + return options + + raise ValueError(f"Plugin service returned no options for parameter '{parameter}' in provider '{provider}'") diff --git a/api/core/plugin/impl/oauth.py b/api/core/plugin/impl/oauth.py index 91774984c8..b006bf1d4b 100644 --- a/api/core/plugin/impl/oauth.py +++ b/api/core/plugin/impl/oauth.py @@ -1,3 +1,4 @@ +import binascii from collections.abc import Mapping from typing import Any @@ -16,7 +17,7 @@ class OAuthHandler(BasePluginClient): provider: str, system_credentials: Mapping[str, Any], ) -> PluginOAuthAuthorizationUrlResponse: - return self._request_with_plugin_daemon_response( + response = self._request_with_plugin_daemon_response_stream( "POST", f"plugin/{tenant_id}/dispatch/oauth/get_authorization_url", PluginOAuthAuthorizationUrlResponse, @@ -32,6 +33,9 @@ class OAuthHandler(BasePluginClient): "Content-Type": "application/json", }, ) + for resp in response: + return resp + raise ValueError("No response received from plugin daemon for authorization URL request.") def get_credentials( self, @@ -49,7 +53,7 @@ class OAuthHandler(BasePluginClient): # encode request to raw http request raw_request_bytes = self._convert_request_to_raw_data(request) - return self._request_with_plugin_daemon_response( + response = self._request_with_plugin_daemon_response_stream( "POST", f"plugin/{tenant_id}/dispatch/oauth/get_credentials", PluginOAuthCredentialsResponse, @@ -58,7 +62,8 @@ class OAuthHandler(BasePluginClient): "data": { "provider": provider, "system_credentials": system_credentials, - "raw_request_bytes": raw_request_bytes, + # for json serialization + "raw_http_request": binascii.hexlify(raw_request_bytes).decode(), }, }, headers={ @@ -66,6 +71,9 @@ class OAuthHandler(BasePluginClient): "Content-Type": "application/json", }, ) + for resp in response: + return resp + raise ValueError("No response received from plugin daemon for authorization URL request.") def _convert_request_to_raw_data(self, request: Request) -> bytes: """ @@ -79,7 +87,7 @@ class OAuthHandler(BasePluginClient): """ # Start with the request line method = request.method - path = request.path + path = request.full_path protocol = request.headers.get("HTTP_VERSION", "HTTP/1.1") raw_data = f"{method} {path} {protocol}\r\n".encode() diff --git a/api/core/plugin/impl/plugin.py b/api/core/plugin/impl/plugin.py index 1cd2dc1be7..04ac8c9649 100644 --- a/api/core/plugin/impl/plugin.py +++ b/api/core/plugin/impl/plugin.py @@ -10,10 +10,10 @@ from core.plugin.entities.plugin import ( PluginInstallationSource, ) from core.plugin.entities.plugin_daemon import ( + PluginDecodeResponse, PluginInstallTask, PluginInstallTaskStartResponse, PluginListResponse, - PluginUploadResponse, ) from core.plugin.impl.base import BasePluginClient @@ -36,7 +36,7 @@ class PluginInstaller(BasePluginClient): "GET", f"plugin/{tenant_id}/management/list", PluginListResponse, - params={"page": 1, "page_size": 256}, + params={"page": 1, "page_size": 256, "response_type": "paged"}, ) return result.list @@ -45,7 +45,7 @@ class PluginInstaller(BasePluginClient): "GET", f"plugin/{tenant_id}/management/list", PluginListResponse, - params={"page": page, "page_size": page_size}, + params={"page": page, "page_size": page_size, "response_type": "paged"}, ) def upload_pkg( @@ -53,7 +53,7 @@ class PluginInstaller(BasePluginClient): tenant_id: str, pkg: bytes, verify_signature: bool = False, - ) -> PluginUploadResponse: + ) -> PluginDecodeResponse: """ Upload a plugin package and return the plugin unique identifier. """ @@ -68,7 +68,7 @@ class PluginInstaller(BasePluginClient): return self._request_with_plugin_daemon_response( "POST", f"plugin/{tenant_id}/management/install/upload/package", - PluginUploadResponse, + PluginDecodeResponse, files=body, data=data, ) @@ -176,6 +176,18 @@ class PluginInstaller(BasePluginClient): params={"plugin_unique_identifier": plugin_unique_identifier}, ) + def decode_plugin_from_identifier(self, tenant_id: str, plugin_unique_identifier: str) -> PluginDecodeResponse: + """ + Decode a plugin from an identifier. + """ + return self._request_with_plugin_daemon_response( + "GET", + f"plugin/{tenant_id}/management/decode/from_identifier", + PluginDecodeResponse, + data={"plugin_unique_identifier": plugin_unique_identifier}, + headers={"Content-Type": "application/json"}, + ) + def fetch_plugin_installation_by_ids( self, tenant_id: str, plugin_ids: Sequence[str] ) -> Sequence[PluginInstallation]: diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py index 25964ae063..0f0fe65f27 100644 --- a/api/core/prompt/advanced_prompt_transform.py +++ b/api/core/prompt/advanced_prompt_transform.py @@ -158,7 +158,7 @@ class AdvancedPromptTransform(PromptTransform): if prompt_item.edition_type == "basic" or not prompt_item.edition_type: if self.with_variable_tmpl: - vp = VariablePool() + vp = VariablePool.empty() for k, v in inputs.items(): if k.startswith("#"): vp.add(k[1:-1].split("."), v) diff --git a/api/core/prompt/utils/extract_thread_messages.py b/api/core/prompt/utils/extract_thread_messages.py index f7aef76c87..4b883622a7 100644 --- a/api/core/prompt/utils/extract_thread_messages.py +++ b/api/core/prompt/utils/extract_thread_messages.py @@ -1,10 +1,11 @@ -from typing import Any +from collections.abc import Sequence from constants import UUID_NIL +from models import Message -def extract_thread_messages(messages: list[Any]): - thread_messages = [] +def extract_thread_messages(messages: Sequence[Message]): + thread_messages: list[Message] = [] next_message = None for message in messages: diff --git a/api/core/prompt/utils/get_thread_messages_length.py b/api/core/prompt/utils/get_thread_messages_length.py index f49466db6d..de64c27a73 100644 --- a/api/core/prompt/utils/get_thread_messages_length.py +++ b/api/core/prompt/utils/get_thread_messages_length.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from core.prompt.utils.extract_thread_messages import extract_thread_messages from extensions.ext_database import db from models.model import Message @@ -8,19 +10,9 @@ def get_thread_messages_length(conversation_id: str) -> int: Get the number of thread messages based on the parent message id. """ # Fetch all messages related to the conversation - query = ( - db.session.query( - Message.id, - Message.parent_message_id, - Message.answer, - ) - .filter( - Message.conversation_id == conversation_id, - ) - .order_by(Message.created_at.desc()) - ) + stmt = select(Message).where(Message.conversation_id == conversation_id).order_by(Message.created_at.desc()) - messages = query.all() + messages = db.session.scalars(stmt).all() # Extract thread messages thread_messages = extract_thread_messages(messages) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 2c5178241c..5a6903d3d5 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -3,7 +3,7 @@ from concurrent.futures import ThreadPoolExecutor from typing import Optional from flask import Flask, current_app -from sqlalchemy.orm import load_only +from sqlalchemy.orm import Session, load_only from configs import dify_config from core.rag.data_post_processor.data_post_processor import DataPostProcessor @@ -144,7 +144,8 @@ class RetrievalService: @classmethod def _get_dataset(cls, dataset_id: str) -> Optional[Dataset]: - return db.session.query(Dataset).filter(Dataset.id == dataset_id).first() + with Session(db.engine) as session: + return session.query(Dataset).filter(Dataset.id == dataset_id).first() @classmethod def keyword_search( diff --git a/api/core/rag/datasource/vdb/matrixone/__init__.py b/api/core/rag/datasource/vdb/matrixone/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py b/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py new file mode 100644 index 0000000000..4894957382 --- /dev/null +++ b/api/core/rag/datasource/vdb/matrixone/matrixone_vector.py @@ -0,0 +1,233 @@ +import json +import logging +import uuid +from functools import wraps +from typing import Any, Optional + +from mo_vector.client import MoVectorClient # type: ignore +from pydantic import BaseModel, model_validator + +from configs import dify_config +from core.rag.datasource.vdb.vector_base import BaseVector +from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory +from core.rag.datasource.vdb.vector_type import VectorType +from core.rag.embedding.embedding_base import Embeddings +from core.rag.models.document import Document +from extensions.ext_redis import redis_client +from models.dataset import Dataset + +logger = logging.getLogger(__name__) + + +class MatrixoneConfig(BaseModel): + host: str = "localhost" + port: int = 6001 + user: str = "dump" + password: str = "111" + database: str = "dify" + metric: str = "l2" + + @model_validator(mode="before") + @classmethod + def validate_config(cls, values: dict) -> dict: + if not values["host"]: + raise ValueError("config host is required") + if not values["port"]: + raise ValueError("config port is required") + if not values["user"]: + raise ValueError("config user is required") + if not values["password"]: + raise ValueError("config password is required") + if not values["database"]: + raise ValueError("config database is required") + return values + + +def ensure_client(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + if self.client is None: + self.client = self._get_client(None, False) + return func(self, *args, **kwargs) + + return wrapper + + +class MatrixoneVector(BaseVector): + """ + Matrixone vector storage implementation. + """ + + def __init__(self, collection_name: str, config: MatrixoneConfig): + super().__init__(collection_name) + self.config = config + self.collection_name = collection_name.lower() + self.client = None + + @property + def collection_name(self): + return self._collection_name + + @collection_name.setter + def collection_name(self, value): + self._collection_name = value + + def get_type(self) -> str: + return VectorType.MATRIXONE + + def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs): + if self.client is None: + self.client = self._get_client(len(embeddings[0]), True) + return self.add_texts(texts, embeddings) + + def _get_client(self, dimension: Optional[int] = None, create_table: bool = False) -> MoVectorClient: + """ + Create a new client for the collection. + + The collection will be created if it doesn't exist. + """ + lock_name = f"vector_indexing_lock_{self._collection_name}" + with redis_client.lock(lock_name, timeout=20): + client = MoVectorClient( + connection_string=f"mysql+pymysql://{self.config.user}:{self.config.password}@{self.config.host}:{self.config.port}/{self.config.database}", + table_name=self.collection_name, + vector_dimension=dimension, + create_table=create_table, + ) + collection_exist_cache_key = f"vector_indexing_{self._collection_name}" + if redis_client.get(collection_exist_cache_key): + return client + try: + client.create_full_text_index() + except Exception as e: + logger.exception("Failed to create full text index") + redis_client.set(collection_exist_cache_key, 1, ex=3600) + return client + + def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): + if self.client is None: + self.client = self._get_client(len(embeddings[0]), True) + assert self.client is not None + ids = [] + for _, doc in enumerate(documents): + if doc.metadata is not None: + doc_id = doc.metadata.get("doc_id", str(uuid.uuid4())) + ids.append(doc_id) + self.client.insert( + texts=[doc.page_content for doc in documents], + embeddings=embeddings, + metadatas=[doc.metadata for doc in documents], + ids=ids, + ) + return ids + + @ensure_client + def text_exists(self, id: str) -> bool: + assert self.client is not None + result = self.client.get(ids=[id]) + return len(result) > 0 + + @ensure_client + def delete_by_ids(self, ids: list[str]) -> None: + assert self.client is not None + if not ids: + return + self.client.delete(ids=ids) + + @ensure_client + def get_ids_by_metadata_field(self, key: str, value: str): + assert self.client is not None + results = self.client.query_by_metadata(filter={key: value}) + return [result.id for result in results] + + @ensure_client + def delete_by_metadata_field(self, key: str, value: str) -> None: + assert self.client is not None + self.client.delete(filter={key: value}) + + @ensure_client + def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: + assert self.client is not None + top_k = kwargs.get("top_k", 5) + document_ids_filter = kwargs.get("document_ids_filter") + filter = None + if document_ids_filter: + filter = {"document_id": {"$in": document_ids_filter}} + + results = self.client.query( + query_vector=query_vector, + k=top_k, + filter=filter, + ) + + docs = [] + # TODO: add the score threshold to the query + for result in results: + metadata = result.metadata + docs.append( + Document( + page_content=result.document, + metadata=metadata, + ) + ) + return docs + + @ensure_client + def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: + assert self.client is not None + top_k = kwargs.get("top_k", 5) + document_ids_filter = kwargs.get("document_ids_filter") + filter = None + if document_ids_filter: + filter = {"document_id": {"$in": document_ids_filter}} + score_threshold = float(kwargs.get("score_threshold", 0.0)) + + results = self.client.full_text_query( + keywords=[query], + k=top_k, + filter=filter, + ) + + docs = [] + for result in results: + metadata = result.metadata + if isinstance(metadata, str): + import json + + metadata = json.loads(metadata) + score = 1 - result.distance + if score >= score_threshold: + metadata["score"] = score + docs.append( + Document( + page_content=result.document, + metadata=metadata, + ) + ) + return docs + + @ensure_client + def delete(self) -> None: + assert self.client is not None + self.client.delete() + + +class MatrixoneVectorFactory(AbstractVectorFactory): + def init_vector(self, dataset: Dataset, attributes: list, embeddings: Embeddings) -> MatrixoneVector: + if dataset.index_struct_dict: + class_prefix: str = dataset.index_struct_dict["vector_store"]["class_prefix"] + collection_name = class_prefix + else: + dataset_id = dataset.id + collection_name = Dataset.gen_collection_name_by_id(dataset_id) + dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.MATRIXONE, collection_name)) + + config = MatrixoneConfig( + host=dify_config.MATRIXONE_HOST or "localhost", + port=dify_config.MATRIXONE_PORT or 6001, + user=dify_config.MATRIXONE_USER or "dump", + password=dify_config.MATRIXONE_PASSWORD or "111", + database=dify_config.MATRIXONE_DATABASE or "dify", + metric=dify_config.MATRIXONE_METRIC or "l2", + ) + return MatrixoneVector(collection_name=collection_name, config=config) diff --git a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py index 2b47d179d2..dd196e1f09 100644 --- a/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py +++ b/api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py @@ -80,6 +80,23 @@ class OceanBaseVector(BaseVector): self.delete() + vals = [] + params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'") + for row in params: + val = int(row[6]) + vals.append(val) + if len(vals) == 0: + raise ValueError("ob_vector_memory_limit_percentage not found in parameters.") + if any(val == 0 for val in vals): + try: + self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30") + except Exception as e: + raise Exception( + "Failed to set ob_vector_memory_limit_percentage. " + + "Maybe the database user has insufficient privilege.", + e, + ) + cols = [ Column("id", String(36), primary_key=True, autoincrement=False), Column("vector", VECTOR(self._vec_dim)), @@ -110,22 +127,6 @@ class OceanBaseVector(BaseVector): + "to support fulltext index and vector index in the same table", e, ) - vals = [] - params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'") - for row in params: - val = int(row[6]) - vals.append(val) - if len(vals) == 0: - raise ValueError("ob_vector_memory_limit_percentage not found in parameters.") - if any(val == 0 for val in vals): - try: - self._client.perform_raw_text_sql("ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30") - except Exception as e: - raise Exception( - "Failed to set ob_vector_memory_limit_percentage. " - + "Maybe the database user has insufficient privilege.", - e, - ) redis_client.set(collection_exist_cache_key, 1, ex=3600) def _check_hybrid_search_support(self) -> bool: diff --git a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py index 6991598ce6..0abb3c0077 100644 --- a/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py +++ b/api/core/rag/datasource/vdb/opensearch/opensearch_vector.py @@ -184,7 +184,16 @@ class OpenSearchVector(BaseVector): } document_ids_filter = kwargs.get("document_ids_filter") if document_ids_filter: - query["query"] = {"terms": {"metadata.document_id": document_ids_filter}} + query["query"] = { + "script_score": { + "query": {"bool": {"filter": [{"terms": {Field.DOCUMENT_ID.value: document_ids_filter}}]}}, + "script": { + "source": "knn_score", + "lang": "knn", + "params": {"field": Field.VECTOR.value, "query_value": query_vector, "space_type": "l2"}, + }, + } + } try: response = self._client.search(index=self._collection_name.lower(), body=query) @@ -209,10 +218,10 @@ class OpenSearchVector(BaseVector): return docs def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - full_text_query = {"query": {"match": {Field.CONTENT_KEY.value: query}}} + full_text_query = {"query": {"bool": {"must": [{"match": {Field.CONTENT_KEY.value: query}}]}}} document_ids_filter = kwargs.get("document_ids_filter") if document_ids_filter: - full_text_query["query"]["terms"] = {"metadata.document_id": document_ids_filter} + full_text_query["query"]["bool"]["filter"] = [{"terms": {"metadata.document_id": document_ids_filter}}] response = self._client.search(index=self._collection_name.lower(), body=full_text_query) @@ -255,7 +264,8 @@ class OpenSearchVector(BaseVector): Field.METADATA_KEY.value: { "type": "object", "properties": { - "doc_id": {"type": "keyword"} # Map doc_id to keyword type + "doc_id": {"type": "keyword"}, # Map doc_id to keyword type + "document_id": {"type": "keyword"}, }, }, } diff --git a/api/core/rag/datasource/vdb/oracle/oraclevector.py b/api/core/rag/datasource/vdb/oracle/oraclevector.py index 6b9dd9c561..d1c8142b3d 100644 --- a/api/core/rag/datasource/vdb/oracle/oraclevector.py +++ b/api/core/rag/datasource/vdb/oracle/oraclevector.py @@ -303,7 +303,6 @@ class OracleVector(BaseVector): return docs else: return [Document(page_content="", metadata={})] - return [] def delete(self) -> None: with self._get_connection() as conn: diff --git a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py index 8ce194c683..05fa73011a 100644 --- a/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py +++ b/api/core/rag/datasource/vdb/qdrant/qdrant_vector.py @@ -47,6 +47,7 @@ class QdrantConfig(BaseModel): grpc_port: int = 6334 prefer_grpc: bool = False replication_factor: int = 1 + write_consistency_factor: int = 1 def to_qdrant_params(self): if self.endpoint and self.endpoint.startswith("path:"): @@ -127,6 +128,7 @@ class QdrantVector(BaseVector): hnsw_config=hnsw_config, timeout=int(self._client_config.timeout), replication_factor=self._client_config.replication_factor, + write_consistency_factor=self._client_config.write_consistency_factor, ) # create group_id payload index diff --git a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py index a124faa503..552068c99e 100644 --- a/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py +++ b/api/core/rag/datasource/vdb/tablestore/tablestore_vector.py @@ -4,6 +4,7 @@ from typing import Any, Optional import tablestore # type: ignore from pydantic import BaseModel, model_validator +from tablestore import BatchGetRowRequest, TableInBatchGetRowItem from configs import dify_config from core.rag.datasource.vdb.field import Field @@ -50,6 +51,29 @@ class TableStoreVector(BaseVector): self._index_name = f"{collection_name}_idx" self._tags_field = f"{Field.METADATA_KEY.value}_tags" + def create_collection(self, embeddings: list[list[float]], **kwargs): + dimension = len(embeddings[0]) + self._create_collection(dimension) + + def get_by_ids(self, ids: list[str]) -> list[Document]: + docs = [] + request = BatchGetRowRequest() + columns_to_get = [Field.METADATA_KEY.value, Field.CONTENT_KEY.value] + rows_to_get = [[("id", _id)] for _id in ids] + request.add(TableInBatchGetRowItem(self._table_name, rows_to_get, columns_to_get, None, 1)) + + result = self._tablestore_client.batch_get_row(request) + table_result = result.get_result_by_table(self._table_name) + for item in table_result: + if item.is_ok and item.row: + kv = {k: v for k, v, t in item.row.attribute_columns} + docs.append( + Document( + page_content=kv[Field.CONTENT_KEY.value], metadata=json.loads(kv[Field.METADATA_KEY.value]) + ) + ) + return docs + def get_type(self) -> str: return VectorType.TABLESTORE diff --git a/api/core/rag/datasource/vdb/tencent/tencent_vector.py b/api/core/rag/datasource/vdb/tencent/tencent_vector.py index d2bf3eb92a..75afe0cdb8 100644 --- a/api/core/rag/datasource/vdb/tencent/tencent_vector.py +++ b/api/core/rag/datasource/vdb/tencent/tencent_vector.py @@ -122,7 +122,6 @@ class TencentVector(BaseVector): metric_type, params, ) - index_text = vdb_index.FilterIndex(self.field_text, enum.FieldType.String, enum.IndexType.FILTER) index_metadate = vdb_index.FilterIndex(self.field_metadata, enum.FieldType.Json, enum.IndexType.FILTER) index_sparse_vector = vdb_index.SparseIndex( name="sparse_vector", @@ -130,7 +129,7 @@ class TencentVector(BaseVector): index_type=enum.IndexType.SPARSE_INVERTED, metric_type=enum.MetricType.IP, ) - indexes = [index_id, index_vector, index_text, index_metadate] + indexes = [index_id, index_vector, index_metadate] if self._enable_hybrid_search: indexes.append(index_sparse_vector) try: @@ -149,7 +148,7 @@ class TencentVector(BaseVector): index_metadate = vdb_index.FilterIndex( self.field_metadata, enum.FieldType.String, enum.IndexType.FILTER ) - indexes = [index_id, index_vector, index_text, index_metadate] + indexes = [index_id, index_vector, index_metadate] if self._enable_hybrid_search: indexes.append(index_sparse_vector) self._client.create_collection( diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index 66e002312a..00080b0fae 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -1,3 +1,5 @@ +import logging +import time from abc import ABC, abstractmethod from typing import Any, Optional @@ -13,6 +15,8 @@ from extensions.ext_database import db from extensions.ext_redis import redis_client from models.dataset import Dataset, Whitelist +logger = logging.getLogger(__name__) + class AbstractVectorFactory(ABC): @abstractmethod @@ -164,13 +168,29 @@ class Vector: from core.rag.datasource.vdb.huawei.huawei_cloud_vector import HuaweiCloudVectorFactory return HuaweiCloudVectorFactory + case VectorType.MATRIXONE: + from core.rag.datasource.vdb.matrixone.matrixone_vector import MatrixoneVectorFactory + + return MatrixoneVectorFactory case _: raise ValueError(f"Vector store {vector_type} is not supported.") def create(self, texts: Optional[list] = None, **kwargs): if texts: - embeddings = self._embeddings.embed_documents([document.page_content for document in texts]) - self._vector_processor.create(texts=texts, embeddings=embeddings, **kwargs) + start = time.time() + logger.info(f"start embedding {len(texts)} texts {start}") + batch_size = 1000 + total_batches = len(texts) + batch_size - 1 + for i in range(0, len(texts), batch_size): + batch = texts[i : i + batch_size] + batch_start = time.time() + logger.info(f"Processing batch {i // batch_size + 1}/{total_batches} ({len(batch)} texts)") + batch_embeddings = self._embeddings.embed_documents([document.page_content for document in batch]) + logger.info( + f"Embedding batch {i // batch_size + 1}/{total_batches} took {time.time() - batch_start:.3f}s" + ) + self._vector_processor.create(texts=batch, embeddings=batch_embeddings, **kwargs) + logger.info(f"Embedding {len(texts)} texts took {time.time() - start:.3f}s") def add_texts(self, documents: list[Document], **kwargs): if kwargs.get("duplicate_check", False): diff --git a/api/core/rag/datasource/vdb/vector_type.py b/api/core/rag/datasource/vdb/vector_type.py index 7a81565e37..0d70947b72 100644 --- a/api/core/rag/datasource/vdb/vector_type.py +++ b/api/core/rag/datasource/vdb/vector_type.py @@ -29,3 +29,4 @@ class VectorType(StrEnum): OPENGAUSS = "opengauss" TABLESTORE = "tablestore" HUAWEI_CLOUD = "huawei_cloud" + MATRIXONE = "matrixone" diff --git a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py index 8fe6199517..7a8efb4068 100644 --- a/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py +++ b/api/core/rag/datasource/vdb/weaviate/weaviate_vector.py @@ -41,6 +41,13 @@ class WeaviateVector(BaseVector): weaviate.connect.connection.has_grpc = False + # Fix to minimize the performance impact of the deprecation check in weaviate-client 3.24.0, + # by changing the connection timeout to pypi.org from 1 second to 0.001 seconds. + # TODO: This can be removed once weaviate-client is updated to 3.26.7 or higher, + # which does not contain the deprecation check. + if hasattr(weaviate.connect.connection, "PYPI_TIMEOUT"): + weaviate.connect.connection.PYPI_TIMEOUT = 0.001 + try: client = weaviate.Client( url=config.endpoint, auth_client_secret=auth_config, timeout_config=(5, 60), startup_period=None diff --git a/api/core/rag/embedding/cached_embedding.py b/api/core/rag/embedding/cached_embedding.py index 42fad111ce..f50f9f6b60 100644 --- a/api/core/rag/embedding/cached_embedding.py +++ b/api/core/rag/embedding/cached_embedding.py @@ -139,4 +139,4 @@ class CacheEmbedding(Embeddings): logging.exception(f"Failed to add embedding to redis for the text '{text[:10]}...({len(text)} chars)'") raise ex - return embedding_results + return embedding_results # type: ignore diff --git a/api/core/rag/extractor/firecrawl/firecrawl_app.py b/api/core/rag/extractor/firecrawl/firecrawl_app.py index 836a1398bf..83a4ac651f 100644 --- a/api/core/rag/extractor/firecrawl/firecrawl_app.py +++ b/api/core/rag/extractor/firecrawl/firecrawl_app.py @@ -22,6 +22,7 @@ class FirecrawlApp: "formats": ["markdown"], "onlyMainContent": True, "timeout": 30000, + "integration": "dify", } if params: json_data.update(params) @@ -39,7 +40,7 @@ class FirecrawlApp: def crawl_url(self, url, params=None) -> str: # Documentation: https://docs.firecrawl.dev/api-reference/endpoint/crawl-post headers = self._prepare_headers() - json_data = {"url": url} + json_data = {"url": url, "integration": "dify"} if params: json_data.update(params) response = self._post_request(f"{self.base_url}/v1/crawl", json_data, headers) @@ -49,7 +50,6 @@ class FirecrawlApp: return cast(str, job_id) else: self._handle_error(response, "start crawl job") - # FIXME: unreachable code for mypy return "" # unreachable def check_crawl_status(self, job_id) -> dict[str, Any]: @@ -82,7 +82,6 @@ class FirecrawlApp: ) else: self._handle_error(response, "check crawl status") - # FIXME: unreachable code for mypy return {} # unreachable def _format_crawl_status_response( @@ -126,4 +125,31 @@ class FirecrawlApp: def _handle_error(self, response, action) -> None: error_message = response.json().get("error", "Unknown error occurred") - raise Exception(f"Failed to {action}. Status code: {response.status_code}. Error: {error_message}") + raise Exception(f"Failed to {action}. Status code: {response.status_code}. Error: {error_message}") # type: ignore[return] + + def search(self, query: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + # Documentation: https://docs.firecrawl.dev/api-reference/endpoint/search + headers = self._prepare_headers() + json_data = { + "query": query, + "limit": 5, + "lang": "en", + "country": "us", + "timeout": 60000, + "ignoreInvalidURLs": False, + "scrapeOptions": {}, + "integration": "dify", + } + if params: + json_data.update(params) + response = self._post_request(f"{self.base_url}/v1/search", json_data, headers) + if response.status_code == 200: + response_data = response.json() + if not response_data.get("success"): + raise Exception(f"Search failed. Error: {response_data.get('warning', 'Unknown error')}") + return cast(dict[str, Any], response_data) + elif response.status_code in {402, 409, 500, 429, 408}: + self._handle_error(response, "perform search") + return {} # Avoid additional exception after handling error + else: + raise Exception(f"Failed to perform search. Status code: {response.status_code}") diff --git a/api/core/rag/extractor/helpers.py b/api/core/rag/extractor/helpers.py index 69ca9d5d63..3d2fb55d9a 100644 --- a/api/core/rag/extractor/helpers.py +++ b/api/core/rag/extractor/helpers.py @@ -1,7 +1,6 @@ """Document loader helpers.""" import concurrent.futures -from pathlib import Path from typing import NamedTuple, Optional, cast @@ -16,7 +15,7 @@ class FileEncoding(NamedTuple): """The language of the file.""" -def detect_file_encodings(file_path: str, timeout: int = 5) -> list[FileEncoding]: +def detect_file_encodings(file_path: str, timeout: int = 5, sample_size: int = 1024 * 1024) -> list[FileEncoding]: """Try to detect the file encoding. Returns a list of `FileEncoding` tuples with the detected encodings ordered @@ -25,11 +24,16 @@ def detect_file_encodings(file_path: str, timeout: int = 5) -> list[FileEncoding Args: file_path: The path to the file to detect the encoding for. timeout: The timeout in seconds for the encoding detection. + sample_size: The number of bytes to read for encoding detection. Default is 1MB. + For large files, reading only a sample is sufficient and prevents timeout. """ import chardet def read_and_detect(file_path: str) -> list[dict]: - rawdata = Path(file_path).read_bytes() + with open(file_path, "rb") as f: + # Read only a sample of the file for encoding detection + # This prevents timeout on large files while still providing accurate encoding detection + rawdata = f.read(sample_size) return cast(list[dict], chardet.detect_all(rawdata)) with concurrent.futures.ThreadPoolExecutor() as executor: diff --git a/api/core/rag/extractor/markdown_extractor.py b/api/core/rag/extractor/markdown_extractor.py index 849852ac23..c97765b1dc 100644 --- a/api/core/rag/extractor/markdown_extractor.py +++ b/api/core/rag/extractor/markdown_extractor.py @@ -68,22 +68,17 @@ class MarkdownExtractor(BaseExtractor): continue header_match = re.match(r"^#+\s", line) if header_match: - if current_header is not None: - markdown_tups.append((current_header, current_text)) - + markdown_tups.append((current_header, current_text)) current_header = line current_text = "" else: current_text += line + "\n" markdown_tups.append((current_header, current_text)) - if current_header is not None: - # pass linting, assert keys are defined - markdown_tups = [ - (re.sub(r"#", "", cast(str, key)).strip(), re.sub(r"<.*?>", "", value)) for key, value in markdown_tups - ] - else: - markdown_tups = [(key, re.sub("\n", "", value)) for key, value in markdown_tups] + markdown_tups = [ + (re.sub(r"#", "", cast(str, key)).strip() if key else None, re.sub(r"<.*?>", "", value)) + for key, value in markdown_tups + ] return markdown_tups diff --git a/api/core/rag/extractor/notion_extractor.py b/api/core/rag/extractor/notion_extractor.py index 4e14800d0a..eca955ddd1 100644 --- a/api/core/rag/extractor/notion_extractor.py +++ b/api/core/rag/extractor/notion_extractor.py @@ -79,55 +79,71 @@ class NotionExtractor(BaseExtractor): def _get_notion_database_data(self, database_id: str, query_dict: dict[str, Any] = {}) -> list[Document]: """Get all the pages from a Notion database.""" assert self._notion_access_token is not None, "Notion access token is required" - res = requests.post( - DATABASE_URL_TMPL.format(database_id=database_id), - headers={ - "Authorization": "Bearer " + self._notion_access_token, - "Content-Type": "application/json", - "Notion-Version": "2022-06-28", - }, - json=query_dict, - ) - - data = res.json() database_content = [] - if "results" not in data or data["results"] is None: - return [] - for result in data["results"]: - properties = result["properties"] - data = {} - value: Any - for property_name, property_value in properties.items(): - type = property_value["type"] - if type == "multi_select": - value = [] - multi_select_list = property_value[type] - for multi_select in multi_select_list: - value.append(multi_select["name"]) - elif type in {"rich_text", "title"}: - if len(property_value[type]) > 0: - value = property_value[type][0]["plain_text"] + next_cursor = None + has_more = True + + while has_more: + current_query = query_dict.copy() + if next_cursor: + current_query["start_cursor"] = next_cursor + + res = requests.post( + DATABASE_URL_TMPL.format(database_id=database_id), + headers={ + "Authorization": "Bearer " + self._notion_access_token, + "Content-Type": "application/json", + "Notion-Version": "2022-06-28", + }, + json=current_query, + ) + + response_data = res.json() + + if "results" not in response_data or response_data["results"] is None: + break + + for result in response_data["results"]: + properties = result["properties"] + data = {} + value: Any + for property_name, property_value in properties.items(): + type = property_value["type"] + if type == "multi_select": + value = [] + multi_select_list = property_value[type] + for multi_select in multi_select_list: + value.append(multi_select["name"]) + elif type in {"rich_text", "title"}: + if len(property_value[type]) > 0: + value = property_value[type][0]["plain_text"] + else: + value = "" + elif type in {"select", "status"}: + if property_value[type]: + value = property_value[type]["name"] + else: + value = "" else: - value = "" - elif type in {"select", "status"}: - if property_value[type]: - value = property_value[type]["name"] + value = property_value[type] + data[property_name] = value + row_dict = {k: v for k, v in data.items() if v} + row_content = "" + for key, value in row_dict.items(): + if isinstance(value, dict): + value_dict = {k: v for k, v in value.items() if v} + value_content = "".join(f"{k}:{v} " for k, v in value_dict.items()) + row_content = row_content + f"{key}:{value_content}\n" else: - value = "" - else: - value = property_value[type] - data[property_name] = value - row_dict = {k: v for k, v in data.items() if v} - row_content = "" - for key, value in row_dict.items(): - if isinstance(value, dict): - value_dict = {k: v for k, v in value.items() if v} - value_content = "".join(f"{k}:{v} " for k, v in value_dict.items()) - row_content = row_content + f"{key}:{value_content}\n" - else: - row_content = row_content + f"{key}:{value}\n" - database_content.append(row_content) + row_content = row_content + f"{key}:{value}\n" + database_content.append(row_content) + + has_more = response_data.get("has_more", False) + next_cursor = response_data.get("next_cursor") + + if not database_content: + return [] return [Document(page_content="\n".join(database_content))] diff --git a/api/core/rag/extractor/text_extractor.py b/api/core/rag/extractor/text_extractor.py index b2b51d71d7..a00d328cb1 100644 --- a/api/core/rag/extractor/text_extractor.py +++ b/api/core/rag/extractor/text_extractor.py @@ -36,8 +36,12 @@ class TextExtractor(BaseExtractor): break except UnicodeDecodeError: continue + else: + raise RuntimeError( + f"Decode failed: {self._file_path}, all detected encodings failed. Original error: {e}" + ) else: - raise RuntimeError(f"Error loading {self._file_path}") from e + raise RuntimeError(f"Decode failed: {self._file_path}, specified encoding failed. Original error: {e}") except Exception as e: raise RuntimeError(f"Error loading {self._file_path}") from e diff --git a/api/core/rag/index_processor/processor/paragraph_index_processor.py b/api/core/rag/index_processor/processor/paragraph_index_processor.py index dca84b9041..9b90bd2bb3 100644 --- a/api/core/rag/index_processor/processor/paragraph_index_processor.py +++ b/api/core/rag/index_processor/processor/paragraph_index_processor.py @@ -76,6 +76,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor): if dataset.indexing_technique == "high_quality": vector = Vector(dataset) vector.create(documents) + with_keywords = False if with_keywords: keywords_list = kwargs.get("keywords_list") keyword = Keyword(dataset) @@ -91,6 +92,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor): vector.delete_by_ids(node_ids) else: vector.delete() + with_keywords = False if with_keywords: keyword = Keyword(dataset) if node_ids: diff --git a/api/core/rag/index_processor/processor/qa_index_processor.py b/api/core/rag/index_processor/processor/qa_index_processor.py index 0055625e13..75f3153697 100644 --- a/api/core/rag/index_processor/processor/qa_index_processor.py +++ b/api/core/rag/index_processor/processor/qa_index_processor.py @@ -104,7 +104,7 @@ class QAIndexProcessor(BaseIndexProcessor): def format_by_template(self, file: FileStorage, **kwargs) -> list[Document]: # check file type - if not file.filename.endswith(".csv"): + if not file.filename or not file.filename.lower().endswith(".csv"): raise ValueError("Invalid file type. Only CSV files are allowed") try: diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 6978860529..5c0360b064 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -9,6 +9,7 @@ from typing import Any, Optional, Union, cast from flask import Flask, current_app from sqlalchemy import Float, and_, or_, text from sqlalchemy import cast as sqlalchemy_cast +from sqlalchemy.orm import Session from core.app.app_config.entities import ( DatasetEntity, @@ -496,6 +497,8 @@ class DatasetRetrieval: all_documents = self.calculate_keyword_score(query, all_documents, top_k) elif index_type == "high_quality": all_documents = self.calculate_vector_score(all_documents, top_k, score_threshold) + else: + all_documents = all_documents[:top_k] if top_k else all_documents self._on_query(query, dataset_ids, app_id, user_from, user_id) @@ -596,7 +599,8 @@ class DatasetRetrieval: metadata_condition: Optional[MetadataCondition] = None, ): with flask_app.app_context(): - dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first() + with Session(db.engine) as session: + dataset = session.query(Dataset).filter(Dataset.id == dataset_id).first() if not dataset: return [] @@ -1008,6 +1012,9 @@ class DatasetRetrieval: def _process_metadata_filter_func( self, sequence: int, condition: str, metadata_name: str, value: Optional[Any], filters: list ): + if value is None: + return + key = f"{metadata_name}_{sequence}" key_value = f"{metadata_name}_{sequence}_value" match condition: diff --git a/api/core/rag/retrieval/router/multi_dataset_react_route.py b/api/core/rag/retrieval/router/multi_dataset_react_route.py index f0426ace1f..33a283771d 100644 --- a/api/core/rag/retrieval/router/multi_dataset_react_route.py +++ b/api/core/rag/retrieval/router/multi_dataset_react_route.py @@ -9,7 +9,7 @@ from core.prompt.advanced_prompt_transform import AdvancedPromptTransform from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate from core.rag.retrieval.output_parser.react_output import ReactAction from core.rag.retrieval.output_parser.structured_chat import StructuredChatOutputParser -from core.workflow.nodes.llm import LLMNode +from core.workflow.nodes.llm import llm_utils PREFIX = """Respond to the human as helpfully and accurately as possible. You have access to the following tools:""" @@ -165,7 +165,7 @@ class ReactMultiDatasetRouter: text, usage = self._handle_invoke_result(invoke_result=invoke_result) # deduct quota - LLMNode.deduct_llm_quota(tenant_id=tenant_id, model_instance=model_instance, usage=usage) + llm_utils.deduct_llm_quota(tenant_id=tenant_id, model_instance=model_instance, usage=usage) return text, usage diff --git a/api/core/repositories/__init__.py b/api/core/repositories/__init__.py index 6452317120..052ba1c2cb 100644 --- a/api/core/repositories/__init__.py +++ b/api/core/repositories/__init__.py @@ -5,8 +5,11 @@ This package contains concrete implementations of the repository interfaces defined in the core.workflow.repository package. """ +from core.repositories.factory import DifyCoreRepositoryFactory, RepositoryImportError from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository __all__ = [ + "DifyCoreRepositoryFactory", + "RepositoryImportError", "SQLAlchemyWorkflowNodeExecutionRepository", ] diff --git a/api/core/repositories/factory.py b/api/core/repositories/factory.py new file mode 100644 index 0000000000..4118aa61c7 --- /dev/null +++ b/api/core/repositories/factory.py @@ -0,0 +1,224 @@ +""" +Repository factory for dynamically creating repository instances based on configuration. + +This module provides a Django-like settings system for repository implementations, +allowing users to configure different repository backends through string paths. +""" + +import importlib +import inspect +import logging +from typing import Protocol, Union + +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker + +from configs import dify_config +from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository +from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from models import Account, EndUser +from models.enums import WorkflowRunTriggeredFrom +from models.workflow import WorkflowNodeExecutionTriggeredFrom + +logger = logging.getLogger(__name__) + + +class RepositoryImportError(Exception): + """Raised when a repository implementation cannot be imported or instantiated.""" + + pass + + +class DifyCoreRepositoryFactory: + """ + Factory for creating repository instances based on configuration. + + This factory supports Django-like settings where repository implementations + are specified as module paths (e.g., 'module.submodule.ClassName'). + """ + + @staticmethod + def _import_class(class_path: str) -> type: + """ + Import a class from a module path string. + + Args: + class_path: Full module path to the class (e.g., 'module.submodule.ClassName') + + Returns: + The imported class + + Raises: + RepositoryImportError: If the class cannot be imported + """ + try: + module_path, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_path) + repo_class = getattr(module, class_name) + assert isinstance(repo_class, type) + return repo_class + except (ValueError, ImportError, AttributeError) as e: + raise RepositoryImportError(f"Cannot import repository class '{class_path}': {e}") from e + + @staticmethod + def _validate_repository_interface(repository_class: type, expected_interface: type[Protocol]) -> None: # type: ignore + """ + Validate that a class implements the expected repository interface. + + Args: + repository_class: The class to validate + expected_interface: The expected interface/protocol + + Raises: + RepositoryImportError: If the class doesn't implement the interface + """ + # Check if the class has all required methods from the protocol + required_methods = [ + method + for method in dir(expected_interface) + if not method.startswith("_") and callable(getattr(expected_interface, method, None)) + ] + + missing_methods = [] + for method_name in required_methods: + if not hasattr(repository_class, method_name): + missing_methods.append(method_name) + + if missing_methods: + raise RepositoryImportError( + f"Repository class '{repository_class.__name__}' does not implement required methods " + f"{missing_methods} from interface '{expected_interface.__name__}'" + ) + + @staticmethod + def _validate_constructor_signature(repository_class: type, required_params: list[str]) -> None: + """ + Validate that a repository class constructor accepts required parameters. + + Args: + repository_class: The class to validate + required_params: List of required parameter names + + Raises: + RepositoryImportError: If the constructor doesn't accept required parameters + """ + + try: + # MyPy may flag the line below with the following error: + # + # > Accessing "__init__" on an instance is unsound, since + # > instance.__init__ could be from an incompatible subclass. + # + # Despite this, we need to ensure that the constructor of `repository_class` + # has a compatible signature. + signature = inspect.signature(repository_class.__init__) # type: ignore[misc] + param_names = list(signature.parameters.keys()) + + # Remove 'self' parameter + if "self" in param_names: + param_names.remove("self") + + missing_params = [param for param in required_params if param not in param_names] + if missing_params: + raise RepositoryImportError( + f"Repository class '{repository_class.__name__}' constructor does not accept required parameters: " + f"{missing_params}. Expected parameters: {required_params}" + ) + except Exception as e: + raise RepositoryImportError( + f"Failed to validate constructor signature for '{repository_class.__name__}': {e}" + ) from e + + @classmethod + def create_workflow_execution_repository( + cls, + session_factory: Union[sessionmaker, Engine], + user: Union[Account, EndUser], + app_id: str, + triggered_from: WorkflowRunTriggeredFrom, + ) -> WorkflowExecutionRepository: + """ + Create a WorkflowExecutionRepository instance based on configuration. + + Args: + session_factory: SQLAlchemy sessionmaker or engine + user: Account or EndUser object + app_id: Application ID + triggered_from: Source of the execution trigger + + Returns: + Configured WorkflowExecutionRepository instance + + Raises: + RepositoryImportError: If the configured repository cannot be created + """ + class_path = dify_config.CORE_WORKFLOW_EXECUTION_REPOSITORY + logger.debug(f"Creating WorkflowExecutionRepository from: {class_path}") + + try: + repository_class = cls._import_class(class_path) + cls._validate_repository_interface(repository_class, WorkflowExecutionRepository) + cls._validate_constructor_signature( + repository_class, ["session_factory", "user", "app_id", "triggered_from"] + ) + + return repository_class( # type: ignore[no-any-return] + session_factory=session_factory, + user=user, + app_id=app_id, + triggered_from=triggered_from, + ) + except RepositoryImportError: + # Re-raise our custom errors as-is + raise + except Exception as e: + logger.exception("Failed to create WorkflowExecutionRepository") + raise RepositoryImportError(f"Failed to create WorkflowExecutionRepository from '{class_path}': {e}") from e + + @classmethod + def create_workflow_node_execution_repository( + cls, + session_factory: Union[sessionmaker, Engine], + user: Union[Account, EndUser], + app_id: str, + triggered_from: WorkflowNodeExecutionTriggeredFrom, + ) -> WorkflowNodeExecutionRepository: + """ + Create a WorkflowNodeExecutionRepository instance based on configuration. + + Args: + session_factory: SQLAlchemy sessionmaker or engine + user: Account or EndUser object + app_id: Application ID + triggered_from: Source of the execution trigger + + Returns: + Configured WorkflowNodeExecutionRepository instance + + Raises: + RepositoryImportError: If the configured repository cannot be created + """ + class_path = dify_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY + logger.debug(f"Creating WorkflowNodeExecutionRepository from: {class_path}") + + try: + repository_class = cls._import_class(class_path) + cls._validate_repository_interface(repository_class, WorkflowNodeExecutionRepository) + cls._validate_constructor_signature( + repository_class, ["session_factory", "user", "app_id", "triggered_from"] + ) + + return repository_class( # type: ignore[no-any-return] + session_factory=session_factory, + user=user, + app_id=app_id, + triggered_from=triggered_from, + ) + except RepositoryImportError: + # Re-raise our custom errors as-is + raise + except Exception as e: + logger.exception("Failed to create WorkflowNodeExecutionRepository") + raise RepositoryImportError( + f"Failed to create WorkflowNodeExecutionRepository from '{class_path}': {e}" + ) from e diff --git a/api/core/repositories/sqlalchemy_workflow_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_execution_repository.py index 19086cffff..0b3e5eb424 100644 --- a/api/core/repositories/sqlalchemy_workflow_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_execution_repository.py @@ -16,6 +16,8 @@ from core.workflow.entities.workflow_execution import ( WorkflowType, ) from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository +from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter +from libs.helper import extract_tenant_id from models import ( Account, CreatorUserRole, @@ -66,7 +68,7 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository): ) # Extract tenant_id from user - tenant_id: str | None = user.tenant_id if isinstance(user, EndUser) else user.current_tenant_id + tenant_id = extract_tenant_id(user) if not tenant_id: raise ValueError("User must have a tenant_id or current_tenant_id") self._tenant_id = tenant_id @@ -146,26 +148,17 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository): db_model.workflow_id = domain_model.workflow_id db_model.triggered_from = self._triggered_from - # Check if this is a new record - with self._session_factory() as session: - existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_)) - if not existing: - # For new records, get the next sequence number - stmt = select(WorkflowRun.sequence_number).where( - WorkflowRun.app_id == self._app_id, - WorkflowRun.tenant_id == self._tenant_id, - ) - max_sequence = session.scalar(stmt.order_by(WorkflowRun.sequence_number.desc())) - db_model.sequence_number = (max_sequence or 0) + 1 - else: - # For updates, keep the existing sequence number - db_model.sequence_number = existing.sequence_number + # No sequence number generation needed anymore db_model.type = domain_model.workflow_type db_model.version = domain_model.workflow_version db_model.graph = json.dumps(domain_model.graph) if domain_model.graph else None db_model.inputs = json.dumps(domain_model.inputs) if domain_model.inputs else None - db_model.outputs = json.dumps(domain_model.outputs) if domain_model.outputs else None + db_model.outputs = ( + json.dumps(WorkflowRuntimeTypeConverter().to_json_encodable(domain_model.outputs)) + if domain_model.outputs + else None + ) db_model.status = domain_model.status db_model.error = domain_model.error_message if domain_model.error_message else None db_model.total_tokens = domain_model.total_tokens diff --git a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py index 2f27442616..a5feeb0d7c 100644 --- a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py @@ -19,6 +19,8 @@ from core.workflow.entities.workflow_node_execution import ( ) from core.workflow.nodes.enums import NodeType from core.workflow.repositories.workflow_node_execution_repository import OrderConfig, WorkflowNodeExecutionRepository +from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter +from libs.helper import extract_tenant_id from models import ( Account, CreatorUserRole, @@ -69,7 +71,7 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository) ) # Extract tenant_id from user - tenant_id: str | None = user.tenant_id if isinstance(user, EndUser) else user.current_tenant_id + tenant_id = extract_tenant_id(user) if not tenant_id: raise ValueError("User must have a tenant_id or current_tenant_id") self._tenant_id = tenant_id @@ -146,6 +148,7 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository) if not self._creator_user_role: raise ValueError("created_by_role is required in repository constructor") + json_converter = WorkflowRuntimeTypeConverter() db_model = WorkflowNodeExecutionModel() db_model.id = domain_model.id db_model.tenant_id = self._tenant_id @@ -160,9 +163,17 @@ class SQLAlchemyWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository) db_model.node_id = domain_model.node_id db_model.node_type = domain_model.node_type db_model.title = domain_model.title - db_model.inputs = json.dumps(domain_model.inputs) if domain_model.inputs else None - db_model.process_data = json.dumps(domain_model.process_data) if domain_model.process_data else None - db_model.outputs = json.dumps(domain_model.outputs) if domain_model.outputs else None + db_model.inputs = ( + json.dumps(json_converter.to_json_encodable(domain_model.inputs)) if domain_model.inputs else None + ) + db_model.process_data = ( + json.dumps(json_converter.to_json_encodable(domain_model.process_data)) + if domain_model.process_data + else None + ) + db_model.outputs = ( + json.dumps(json_converter.to_json_encodable(domain_model.outputs)) if domain_model.outputs else None + ) db_model.status = domain_model.status db_model.error = domain_model.error db_model.elapsed_time = domain_model.elapsed_time diff --git a/api/core/tools/builtin_tool/_position.yaml b/api/core/tools/builtin_tool/_position.yaml index b5875e2075..0e811de311 100644 --- a/api/core/tools/builtin_tool/_position.yaml +++ b/api/core/tools/builtin_tool/_position.yaml @@ -1,3 +1,4 @@ +- audio - code - time -- qrcode +- webscraper diff --git a/api/core/tools/builtin_tool/providers/audio/tools/tts.py b/api/core/tools/builtin_tool/providers/audio/tools/tts.py index 9b104b00f5..f191968812 100644 --- a/api/core/tools/builtin_tool/providers/audio/tools/tts.py +++ b/api/core/tools/builtin_tool/providers/audio/tools/tts.py @@ -31,6 +31,14 @@ class TTSTool(BuiltinTool): model_type=ModelType.TTS, model=model, ) + if not voice: + voices = model_instance.get_tts_voices() + if voices: + voice = voices[0].get("value") + if not voice: + raise ValueError("Sorry, no voice available.") + else: + raise ValueError("Sorry, no voice available.") tts = model_instance.invoke_tts( content_text=tool_parameters.get("text"), # type: ignore user=user_id, diff --git a/api/core/tools/builtin_tool/providers/code/tools/simple_code.py b/api/core/tools/builtin_tool/providers/code/tools/simple_code.py index ab0e155b98..b4e650e0ed 100644 --- a/api/core/tools/builtin_tool/providers/code/tools/simple_code.py +++ b/api/core/tools/builtin_tool/providers/code/tools/simple_code.py @@ -4,6 +4,7 @@ from typing import Any, Optional from core.helper.code_executor.code_executor import CodeExecutor, CodeLanguage from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.errors import ToolInvokeError class SimpleCode(BuiltinTool): @@ -25,6 +26,8 @@ class SimpleCode(BuiltinTool): if language not in {CodeLanguage.PYTHON3, CodeLanguage.JAVASCRIPT}: raise ValueError(f"Only python3 and javascript are supported, not {language}") - result = CodeExecutor.execute_code(language, "", code) - - yield self.create_text_message(result) + try: + result = CodeExecutor.execute_code(language, "", code) + yield self.create_text_message(result) + except Exception as e: + raise ToolInvokeError(str(e)) diff --git a/api/core/tools/custom_tool/provider.py b/api/core/tools/custom_tool/provider.py index 3137d32013..fbe1d79137 100644 --- a/api/core/tools/custom_tool/provider.py +++ b/api/core/tools/custom_tool/provider.py @@ -39,19 +39,22 @@ class ApiToolProviderController(ToolProviderController): type=ProviderConfig.Type.SELECT, options=[ ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="无")), - ProviderConfig.Option(value="api_key", label=I18nObject(en_US="api_key", zh_Hans="api_key")), + ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")), + ProviderConfig.Option( + value="api_key_query", label=I18nObject(en_US="Query Param", zh_Hans="查询参数") + ), ], default="none", help=I18nObject(en_US="The auth type of the api provider", zh_Hans="api provider 的认证类型"), ) ] - if auth_type == ApiProviderAuthType.API_KEY: + if auth_type == ApiProviderAuthType.API_KEY_HEADER: credentials_schema = [ *credentials_schema, ProviderConfig( name="api_key_header", required=False, - default="api_key", + default="Authorization", type=ProviderConfig.Type.TEXT_INPUT, help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"), ), @@ -74,6 +77,25 @@ class ApiToolProviderController(ToolProviderController): ], ), ] + elif auth_type == ApiProviderAuthType.API_KEY_QUERY: + credentials_schema = [ + *credentials_schema, + ProviderConfig( + name="api_key_query_param", + required=False, + default="key", + type=ProviderConfig.Type.TEXT_INPUT, + help=I18nObject( + en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称" + ), + ), + ProviderConfig( + name="api_key_value", + required=True, + type=ProviderConfig.Type.SECRET_INPUT, + help=I18nObject(en_US="The api key", zh_Hans="api key 的值"), + ), + ] elif auth_type == ApiProviderAuthType.NONE: pass diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 2f5cc6d4c0..10653b9948 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -78,8 +78,8 @@ class ApiTool(Tool): if "auth_type" not in credentials: raise ToolProviderCredentialValidationError("Missing auth_type") - if credentials["auth_type"] == "api_key": - api_key_header = "api_key" + if credentials["auth_type"] in ("api_key_header", "api_key"): # backward compatibility: + api_key_header = "Authorization" if "api_key_header" in credentials: api_key_header = credentials["api_key_header"] @@ -100,6 +100,11 @@ class ApiTool(Tool): headers[api_key_header] = credentials["api_key_value"] + elif credentials["auth_type"] == "api_key_query": + # For query parameter authentication, we don't add anything to headers + # The query parameter will be added in do_http_request method + pass + needed_parameters = [parameter for parameter in (self.api_bundle.parameters or []) if parameter.required] for parameter in needed_parameters: if parameter.required and parameter.name not in parameters: @@ -154,6 +159,15 @@ class ApiTool(Tool): cookies = {} files = [] + # Add API key to query parameters if auth_type is api_key_query + if self.runtime and self.runtime.credentials: + credentials = self.runtime.credentials + if credentials.get("auth_type") == "api_key_query": + api_key_query_param = credentials.get("api_key_query_param", "key") + api_key_value = credentials.get("api_key_value") + if api_key_value: + params[api_key_query_param] = api_key_value + # check parameters for parameter in self.api_bundle.openapi.get("parameters", []): value = self.get_parameter_value(parameter, parameters) @@ -213,7 +227,8 @@ class ApiTool(Tool): elif "default" in property: body[name] = property["default"] else: - body[name] = None + # omit optional parameters that weren't provided, instead of setting them to None + pass break # replace path parameters diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index b96c994cff..90134ba71d 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -1,4 +1,5 @@ -from typing import Literal, Optional +from datetime import datetime +from typing import Any, Literal, Optional from pydantic import BaseModel, Field, field_validator @@ -18,7 +19,7 @@ class ToolApiEntity(BaseModel): output_schema: Optional[dict] = None -ToolProviderTypeApiLiteral = Optional[Literal["builtin", "api", "workflow"]] +ToolProviderTypeApiLiteral = Optional[Literal["builtin", "api", "workflow", "mcp"]] class ToolProviderApiEntity(BaseModel): @@ -27,6 +28,7 @@ class ToolProviderApiEntity(BaseModel): name: str # identifier description: I18nObject icon: str | dict + icon_dark: Optional[str | dict] = Field(default=None, description="The dark icon of the tool") label: I18nObject # label type: ToolProviderType masked_credentials: Optional[dict] = None @@ -37,6 +39,10 @@ class ToolProviderApiEntity(BaseModel): plugin_unique_identifier: Optional[str] = Field(default="", description="The unique identifier of the tool") tools: list[ToolApiEntity] = Field(default_factory=list) labels: list[str] = Field(default_factory=list) + # MCP + server_url: Optional[str] = Field(default="", description="The server url of the tool") + updated_at: int = Field(default_factory=lambda: int(datetime.now().timestamp())) + server_identifier: Optional[str] = Field(default="", description="The server identifier of the MCP tool") @field_validator("tools", mode="before") @classmethod @@ -52,8 +58,13 @@ class ToolProviderApiEntity(BaseModel): for parameter in tool.get("parameters"): if parameter.get("type") == ToolParameter.ToolParameterType.SYSTEM_FILES.value: parameter["type"] = "files" + if parameter.get("input_schema") is None: + parameter.pop("input_schema", None) # ------------- - + optional_fields = self.optional_field("server_url", self.server_url) + if self.type == ToolProviderType.MCP.value: + optional_fields.update(self.optional_field("updated_at", self.updated_at)) + optional_fields.update(self.optional_field("server_identifier", self.server_identifier)) return { "id": self.id, "author": self.author, @@ -62,6 +73,7 @@ class ToolProviderApiEntity(BaseModel): "plugin_unique_identifier": self.plugin_unique_identifier, "description": self.description.to_dict(), "icon": self.icon, + "icon_dark": self.icon_dark, "label": self.label.to_dict(), "type": self.type.value, "team_credentials": self.masked_credentials, @@ -69,4 +81,9 @@ class ToolProviderApiEntity(BaseModel): "allow_delete": self.allow_delete, "tools": tools, "labels": self.labels, + **optional_fields, } + + def optional_field(self, key: str, value: Any) -> dict: + """Return dict with key-value if value is truthy, empty dict otherwise.""" + return {key: value} if value else {} diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index b720b88758..64568a8eda 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_seriali from core.entities.provider_entities import ProviderConfig from core.plugin.entities.parameters import ( + MCPServerParameterType, PluginParameter, PluginParameterOption, PluginParameterType, @@ -50,6 +51,7 @@ class ToolProviderType(enum.StrEnum): API = "api" APP = "app" DATASET_RETRIEVAL = "dataset-retrieval" + MCP = "mcp" @classmethod def value_of(cls, value: str) -> "ToolProviderType": @@ -95,7 +97,8 @@ class ApiProviderAuthType(Enum): """ NONE = "none" - API_KEY = "api_key" + API_KEY_HEADER = "api_key_header" + API_KEY_QUERY = "api_key_query" @classmethod def value_of(cls, value: str) -> "ApiProviderAuthType": @@ -255,6 +258,11 @@ class ToolParameter(PluginParameter): APP_SELECTOR = PluginParameterType.APP_SELECTOR.value MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR.value ANY = PluginParameterType.ANY.value + DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT.value + + # MCP object and array type parameters + ARRAY = MCPServerParameterType.ARRAY.value + OBJECT = MCPServerParameterType.OBJECT.value # deprecated, should not use. SYSTEM_FILES = PluginParameterType.SYSTEM_FILES.value @@ -274,6 +282,8 @@ class ToolParameter(PluginParameter): human_description: Optional[I18nObject] = Field(default=None, description="The description presented to the user") form: ToolParameterForm = Field(..., description="The form of the parameter, schema/form/llm") llm_description: Optional[str] = None + # MCP object and array type parameters use this field to store the schema + input_schema: Optional[dict] = None @classmethod def get_simple_instance( @@ -323,6 +333,7 @@ class ToolProviderIdentity(BaseModel): name: str = Field(..., description="The name of the tool") description: I18nObject = Field(..., description="The description of the tool") icon: str = Field(..., description="The icon of the tool") + icon_dark: Optional[str] = Field(default=None, description="The dark icon of the tool") label: I18nObject = Field(..., description="The label of the tool") tags: Optional[list[ToolLabelEnum]] = Field( default=[], diff --git a/api/core/tools/mcp_tool/provider.py b/api/core/tools/mcp_tool/provider.py new file mode 100644 index 0000000000..93f003effe --- /dev/null +++ b/api/core/tools/mcp_tool/provider.py @@ -0,0 +1,130 @@ +import json +from typing import Any + +from core.mcp.types import Tool as RemoteMCPTool +from core.tools.__base.tool_provider import ToolProviderController +from core.tools.__base.tool_runtime import ToolRuntime +from core.tools.entities.common_entities import I18nObject +from core.tools.entities.tool_entities import ( + ToolDescription, + ToolEntity, + ToolIdentity, + ToolProviderEntityWithPlugin, + ToolProviderIdentity, + ToolProviderType, +) +from core.tools.mcp_tool.tool import MCPTool +from models.tools import MCPToolProvider +from services.tools.tools_transform_service import ToolTransformService + + +class MCPToolProviderController(ToolProviderController): + provider_id: str + entity: ToolProviderEntityWithPlugin + + def __init__(self, entity: ToolProviderEntityWithPlugin, provider_id: str, tenant_id: str, server_url: str) -> None: + super().__init__(entity) + self.entity = entity + self.tenant_id = tenant_id + self.provider_id = provider_id + self.server_url = server_url + + @property + def provider_type(self) -> ToolProviderType: + """ + returns the type of the provider + + :return: type of the provider + """ + return ToolProviderType.MCP + + @classmethod + def _from_db(cls, db_provider: MCPToolProvider) -> "MCPToolProviderController": + """ + from db provider + """ + tools = [] + tools_data = json.loads(db_provider.tools) + remote_mcp_tools = [RemoteMCPTool(**tool) for tool in tools_data] + user = db_provider.load_user() + tools = [ + ToolEntity( + identity=ToolIdentity( + author=user.name if user else "Anonymous", + name=remote_mcp_tool.name, + label=I18nObject(en_US=remote_mcp_tool.name, zh_Hans=remote_mcp_tool.name), + provider=db_provider.server_identifier, + icon=db_provider.icon, + ), + parameters=ToolTransformService.convert_mcp_schema_to_parameter(remote_mcp_tool.inputSchema), + description=ToolDescription( + human=I18nObject( + en_US=remote_mcp_tool.description or "", zh_Hans=remote_mcp_tool.description or "" + ), + llm=remote_mcp_tool.description or "", + ), + output_schema=None, + has_runtime_parameters=len(remote_mcp_tool.inputSchema) > 0, + ) + for remote_mcp_tool in remote_mcp_tools + ] + + return cls( + entity=ToolProviderEntityWithPlugin( + identity=ToolProviderIdentity( + author=user.name if user else "Anonymous", + name=db_provider.name, + label=I18nObject(en_US=db_provider.name, zh_Hans=db_provider.name), + description=I18nObject(en_US="", zh_Hans=""), + icon=db_provider.icon, + ), + plugin_id=None, + credentials_schema=[], + tools=tools, + ), + provider_id=db_provider.server_identifier or "", + tenant_id=db_provider.tenant_id or "", + server_url=db_provider.decrypted_server_url, + ) + + def _validate_credentials(self, user_id: str, credentials: dict[str, Any]) -> None: + """ + validate the credentials of the provider + """ + pass + + def get_tool(self, tool_name: str) -> MCPTool: # type: ignore + """ + return tool with given name + """ + tool_entity = next( + (tool_entity for tool_entity in self.entity.tools if tool_entity.identity.name == tool_name), None + ) + + if not tool_entity: + raise ValueError(f"Tool with name {tool_name} not found") + + return MCPTool( + entity=tool_entity, + runtime=ToolRuntime(tenant_id=self.tenant_id), + tenant_id=self.tenant_id, + icon=self.entity.identity.icon, + server_url=self.server_url, + provider_id=self.provider_id, + ) + + def get_tools(self) -> list[MCPTool]: # type: ignore + """ + get all tools + """ + return [ + MCPTool( + entity=tool_entity, + runtime=ToolRuntime(tenant_id=self.tenant_id), + tenant_id=self.tenant_id, + icon=self.entity.identity.icon, + server_url=self.server_url, + provider_id=self.provider_id, + ) + for tool_entity in self.entity.tools + ] diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py new file mode 100644 index 0000000000..d1bacbc735 --- /dev/null +++ b/api/core/tools/mcp_tool/tool.py @@ -0,0 +1,92 @@ +import base64 +import json +from collections.abc import Generator +from typing import Any, Optional + +from core.mcp.error import MCPAuthError, MCPConnectionError +from core.mcp.mcp_client import MCPClient +from core.mcp.types import ImageContent, TextContent +from core.tools.__base.tool import Tool +from core.tools.__base.tool_runtime import ToolRuntime +from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolParameter, ToolProviderType + + +class MCPTool(Tool): + tenant_id: str + icon: str + runtime_parameters: Optional[list[ToolParameter]] + server_url: str + provider_id: str + + def __init__( + self, entity: ToolEntity, runtime: ToolRuntime, tenant_id: str, icon: str, server_url: str, provider_id: str + ) -> None: + super().__init__(entity, runtime) + self.tenant_id = tenant_id + self.icon = icon + self.runtime_parameters = None + self.server_url = server_url + self.provider_id = provider_id + + def tool_provider_type(self) -> ToolProviderType: + return ToolProviderType.MCP + + def _invoke( + self, + user_id: str, + tool_parameters: dict[str, Any], + conversation_id: Optional[str] = None, + app_id: Optional[str] = None, + message_id: Optional[str] = None, + ) -> Generator[ToolInvokeMessage, None, None]: + from core.tools.errors import ToolInvokeError + + try: + with MCPClient(self.server_url, self.provider_id, self.tenant_id, authed=True) as mcp_client: + tool_parameters = self._handle_none_parameter(tool_parameters) + result = mcp_client.invoke_tool(tool_name=self.entity.identity.name, tool_args=tool_parameters) + except MCPAuthError as e: + raise ToolInvokeError("Please auth the tool first") from e + except MCPConnectionError as e: + raise ToolInvokeError(f"Failed to connect to MCP server: {e}") from e + except Exception as e: + raise ToolInvokeError(f"Failed to invoke tool: {e}") from e + + for content in result.content: + if isinstance(content, TextContent): + try: + content_json = json.loads(content.text) + if isinstance(content_json, dict): + yield self.create_json_message(content_json) + elif isinstance(content_json, list): + for item in content_json: + yield self.create_json_message(item) + else: + yield self.create_text_message(content.text) + except json.JSONDecodeError: + yield self.create_text_message(content.text) + + elif isinstance(content, ImageContent): + yield self.create_blob_message( + blob=base64.b64decode(content.data), meta={"mime_type": content.mimeType} + ) + + def fork_tool_runtime(self, runtime: ToolRuntime) -> "MCPTool": + return MCPTool( + entity=self.entity, + runtime=runtime, + tenant_id=self.tenant_id, + icon=self.icon, + server_url=self.server_url, + provider_id=self.provider_id, + ) + + def _handle_none_parameter(self, parameter: dict[str, Any]) -> dict[str, Any]: + """ + in mcp tool invoke, if the parameter is empty, it will be set to None + """ + return { + key: value + for key, value in parameter.items() + if value is not None and not (isinstance(value, str) and value.strip() == "") + } diff --git a/api/core/tools/signature.py b/api/core/tools/signature.py index e80005d7bf..5cdf473542 100644 --- a/api/core/tools/signature.py +++ b/api/core/tools/signature.py @@ -9,9 +9,10 @@ from configs import dify_config def sign_tool_file(tool_file_id: str, extension: str) -> str: """ - sign file to get a temporary url + sign file to get a temporary url for plugin access """ - base_url = dify_config.FILES_URL + # Use internal URL for plugin/tool file access in Docker environments + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}" timestamp = str(int(time.time())) diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py index b849f51064..ece02f9d59 100644 --- a/api/core/tools/tool_file_manager.py +++ b/api/core/tools/tool_file_manager.py @@ -35,9 +35,10 @@ class ToolFileManager: @staticmethod def sign_file(tool_file_id: str, extension: str) -> str: """ - sign file to get a temporary url + sign file to get a temporary url for plugin access """ - base_url = dify_config.FILES_URL + # Use internal URL for plugin/tool file access in Docker environments + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}" timestamp = str(int(time.time())) diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index 0bfe6329b1..22a9853b41 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -4,7 +4,7 @@ import mimetypes from collections.abc import Generator from os import listdir, path from threading import Lock -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast from yarl import URL @@ -13,9 +13,13 @@ from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.tool import PluginToolManager from core.tools.__base.tool_provider import ToolProviderController from core.tools.__base.tool_runtime import ToolRuntime +from core.tools.mcp_tool.provider import MCPToolProviderController +from core.tools.mcp_tool.tool import MCPTool from core.tools.plugin_tool.provider import PluginToolProviderController from core.tools.plugin_tool.tool import PluginTool from core.tools.workflow_as_tool.provider import WorkflowToolProviderController +from core.workflow.entities.variable_pool import VariablePool +from services.tools.mcp_tools_mange_service import MCPToolManageService if TYPE_CHECKING: from core.workflow.nodes.tool.entities import ToolEntity @@ -49,7 +53,7 @@ from core.tools.utils.configuration import ( ) from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db -from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider +from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider from services.tools.tools_transform_service import ToolTransformService logger = logging.getLogger(__name__) @@ -156,7 +160,7 @@ class ToolManager: tenant_id: str, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, tool_invoke_from: ToolInvokeFrom = ToolInvokeFrom.AGENT, - ) -> Union[BuiltinTool, PluginTool, ApiTool, WorkflowTool]: + ) -> Union[BuiltinTool, PluginTool, ApiTool, WorkflowTool, MCPTool]: """ get the tool runtime @@ -292,6 +296,8 @@ class ToolManager: raise NotImplementedError("app provider not implemented") elif provider_type == ToolProviderType.PLUGIN: return cls.get_plugin_provider(provider_id, tenant_id).get_tool(tool_name) + elif provider_type == ToolProviderType.MCP: + return cls.get_mcp_provider_controller(tenant_id, provider_id).get_tool(tool_name) else: raise ToolProviderNotFoundError(f"provider type {provider_type.value} not found") @@ -302,6 +308,7 @@ class ToolManager: app_id: str, agent_tool: AgentToolEntity, invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, + variable_pool: Optional[VariablePool] = None, ) -> Tool: """ get the agent tool runtime @@ -316,24 +323,9 @@ class ToolManager: ) runtime_parameters = {} parameters = tool_entity.get_merged_runtime_parameters() - for parameter in parameters: - # check file types - if ( - parameter.type - in { - ToolParameter.ToolParameterType.SYSTEM_FILES, - ToolParameter.ToolParameterType.FILE, - ToolParameter.ToolParameterType.FILES, - } - and parameter.required - ): - raise ValueError(f"file type parameter {parameter.name} not supported in agent") - - if parameter.form == ToolParameter.ToolParameterForm.FORM: - # save tool parameter to tool entity memory - value = parameter.init_frontend_parameter(agent_tool.tool_parameters.get(parameter.name)) - runtime_parameters[parameter.name] = value - + runtime_parameters = cls._convert_tool_parameters_type( + parameters, variable_pool, agent_tool.tool_parameters, typ="agent" + ) # decrypt runtime parameters encryption_manager = ToolParameterConfigurationManager( tenant_id=tenant_id, @@ -357,10 +349,12 @@ class ToolManager: node_id: str, workflow_tool: "ToolEntity", invoke_from: InvokeFrom = InvokeFrom.DEBUGGER, + variable_pool: Optional[VariablePool] = None, ) -> Tool: """ get the workflow tool runtime """ + tool_runtime = cls.get_tool_runtime( provider_type=workflow_tool.provider_type, provider_id=workflow_tool.provider_id, @@ -369,15 +363,11 @@ class ToolManager: invoke_from=invoke_from, tool_invoke_from=ToolInvokeFrom.WORKFLOW, ) - runtime_parameters = {} - parameters = tool_runtime.get_merged_runtime_parameters() - - for parameter in parameters: - # save tool parameter to tool entity memory - if parameter.form == ToolParameter.ToolParameterForm.FORM: - value = parameter.init_frontend_parameter(workflow_tool.tool_configurations.get(parameter.name)) - runtime_parameters[parameter.name] = value + parameters = tool_runtime.get_merged_runtime_parameters() + runtime_parameters = cls._convert_tool_parameters_type( + parameters, variable_pool, workflow_tool.tool_configurations, typ="workflow" + ) # decrypt runtime parameters encryption_manager = ToolParameterConfigurationManager( tenant_id=tenant_id, @@ -569,7 +559,7 @@ class ToolManager: filters = [] if not typ: - filters.extend(["builtin", "api", "workflow"]) + filters.extend(["builtin", "api", "workflow", "mcp"]) else: filters.append(typ) @@ -663,6 +653,10 @@ class ToolManager: labels=labels.get(provider_controller.provider_id, []), ) result_providers[f"workflow_provider.{user_provider.name}"] = user_provider + if "mcp" in filters: + mcp_providers = MCPToolManageService.retrieve_mcp_tools(tenant_id, for_list=True) + for mcp_provider in mcp_providers: + result_providers[f"mcp_provider.{mcp_provider.name}"] = mcp_provider return BuiltinToolProviderSort.sort(list(result_providers.values())) @@ -690,14 +684,47 @@ class ToolManager: if provider is None: raise ToolProviderNotFoundError(f"api provider {provider_id} not found") + auth_type = ApiProviderAuthType.NONE + provider_auth_type = provider.credentials.get("auth_type") + if provider_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif provider_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( provider, - ApiProviderAuthType.API_KEY if provider.credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE, + auth_type, ) controller.load_bundled_tools(provider.tools) return controller, provider.credentials + @classmethod + def get_mcp_provider_controller(cls, tenant_id: str, provider_id: str) -> MCPToolProviderController: + """ + get the api provider + + :param tenant_id: the id of the tenant + :param provider_id: the id of the provider + + :return: the provider controller, the credentials + """ + provider: MCPToolProvider | None = ( + db.session.query(MCPToolProvider) + .filter( + MCPToolProvider.server_identifier == provider_id, + MCPToolProvider.tenant_id == tenant_id, + ) + .first() + ) + + if provider is None: + raise ToolProviderNotFoundError(f"mcp provider {provider_id} not found") + + controller = MCPToolProviderController._from_db(provider) + + return controller + @classmethod def user_get_api_provider(cls, provider: str, tenant_id: str) -> dict: """ @@ -725,9 +752,16 @@ class ToolManager: credentials = {} # package tool provider controller + auth_type = ApiProviderAuthType.NONE + credentials_auth_type = credentials.get("auth_type") + if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif credentials_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( provider_obj, - ApiProviderAuthType.API_KEY if credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE, + auth_type, ) # init tool configuration tool_configuration = ProviderConfigEncrypter( @@ -826,6 +860,22 @@ class ToolManager: except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} + @classmethod + def generate_mcp_tool_icon_url(cls, tenant_id: str, provider_id: str) -> dict[str, str] | str: + try: + mcp_provider: MCPToolProvider | None = ( + db.session.query(MCPToolProvider) + .filter(MCPToolProvider.tenant_id == tenant_id, MCPToolProvider.server_identifier == provider_id) + .first() + ) + + if mcp_provider is None: + raise ToolProviderNotFoundError(f"mcp provider {provider_id} not found") + + return mcp_provider.provider_icon + except Exception: + return {"background": "#252525", "content": "\ud83d\ude01"} + @classmethod def get_tool_icon( cls, @@ -863,8 +913,61 @@ class ToolManager: except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} raise ValueError(f"plugin provider {provider_id} not found") + elif provider_type == ToolProviderType.MCP: + return cls.generate_mcp_tool_icon_url(tenant_id, provider_id) else: raise ValueError(f"provider type {provider_type} not found") + @classmethod + def _convert_tool_parameters_type( + cls, + parameters: list[ToolParameter], + variable_pool: Optional[VariablePool], + tool_configurations: dict[str, Any], + typ: Literal["agent", "workflow", "tool"] = "workflow", + ) -> dict[str, Any]: + """ + Convert tool parameters type + """ + from core.workflow.nodes.tool.entities import ToolNodeData + from core.workflow.nodes.tool.exc import ToolParameterError + + runtime_parameters = {} + for parameter in parameters: + if ( + parameter.type + in { + ToolParameter.ToolParameterType.SYSTEM_FILES, + ToolParameter.ToolParameterType.FILE, + ToolParameter.ToolParameterType.FILES, + } + and parameter.required + and typ == "agent" + ): + raise ValueError(f"file type parameter {parameter.name} not supported in agent") + # save tool parameter to tool entity memory + if parameter.form == ToolParameter.ToolParameterForm.FORM: + if variable_pool: + config = tool_configurations.get(parameter.name, {}) + if not (config and isinstance(config, dict) and config.get("value") is not None): + continue + tool_input = ToolNodeData.ToolInput(**tool_configurations.get(parameter.name, {})) + if tool_input.type == "variable": + variable = variable_pool.get(tool_input.value) + if variable is None: + raise ToolParameterError(f"Variable {tool_input.value} does not exist") + parameter_value = variable.value + elif tool_input.type in {"mixed", "constant"}: + segment_group = variable_pool.convert_template(str(tool_input.value)) + parameter_value = segment_group.text + else: + raise ToolParameterError(f"Unknown tool input type '{tool_input.type}'") + runtime_parameters[parameter.name] = parameter_value + + else: + value = parameter.init_frontend_parameter(tool_configurations.get(parameter.name)) + runtime_parameters[parameter.name] = value + return runtime_parameters + ToolManager.load_hardcoded_providers_cache() diff --git a/api/core/tools/utils/configuration.py b/api/core/tools/utils/configuration.py index 6a5fba65bd..251fedf56e 100644 --- a/api/core/tools/utils/configuration.py +++ b/api/core/tools/utils/configuration.py @@ -72,20 +72,21 @@ class ProviderConfigEncrypter(BaseModel): return data - def decrypt(self, data: dict[str, str]) -> dict[str, str]: + def decrypt(self, data: dict[str, str], use_cache: bool = True) -> dict[str, str]: """ decrypt tool credentials with tenant id return a deep copy of credentials with decrypted values """ - cache = ToolProviderCredentialsCache( - tenant_id=self.tenant_id, - identity_id=f"{self.provider_type}.{self.provider_identity}", - cache_type=ToolProviderCredentialsCacheType.PROVIDER, - ) - cached_credentials = cache.get() - if cached_credentials: - return cached_credentials + if use_cache: + cache = ToolProviderCredentialsCache( + tenant_id=self.tenant_id, + identity_id=f"{self.provider_type}.{self.provider_identity}", + cache_type=ToolProviderCredentialsCacheType.PROVIDER, + ) + cached_credentials = cache.get() + if cached_credentials: + return cached_credentials data = self._deep_copy(data) # get fields need to be decrypted fields = dict[str, BasicProviderConfig]() @@ -104,7 +105,8 @@ class ProviderConfigEncrypter(BaseModel): except Exception: pass - cache.set(data) + if use_cache: + cache.set(data) return data def delete_tool_credentials_cache(self): diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py index 93d3fcc49d..2cbc4b9821 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py @@ -153,8 +153,6 @@ class DatasetMultiRetrieverTool(DatasetRetrieverBaseTool): return str("\n".join(document_context_list)) return "" - raise RuntimeError("not segments found") - def _retriever( self, flask_app: Flask, diff --git a/api/core/tools/utils/message_transformer.py b/api/core/tools/utils/message_transformer.py index 80e8b54343..9998de0465 100644 --- a/api/core/tools/utils/message_transformer.py +++ b/api/core/tools/utils/message_transformer.py @@ -32,14 +32,14 @@ class ToolFileMessageTransformer: try: assert isinstance(message.message, ToolInvokeMessage.TextMessage) tool_file_manager = ToolFileManager() - file = tool_file_manager.create_file_by_url( + tool_file = tool_file_manager.create_file_by_url( user_id=user_id, tenant_id=tenant_id, file_url=message.message.text, conversation_id=conversation_id, ) - url = f"/files/tools/{file.id}{guess_extension(file.mimetype) or '.png'}" + url = f"/files/tools/{tool_file.id}{guess_extension(tool_file.mimetype) or '.png'}" yield ToolInvokeMessage( type=ToolInvokeMessage.MessageType.IMAGE_LINK, @@ -68,7 +68,7 @@ class ToolFileMessageTransformer: assert isinstance(message.message.blob, bytes) tool_file_manager = ToolFileManager() - file = tool_file_manager.create_file_by_raw( + tool_file = tool_file_manager.create_file_by_raw( user_id=user_id, tenant_id=tenant_id, conversation_id=conversation_id, @@ -77,7 +77,7 @@ class ToolFileMessageTransformer: filename=filename, ) - url = cls.get_tool_file_url(tool_file_id=file.id, extension=guess_extension(file.mimetype)) + url = cls.get_tool_file_url(tool_file_id=tool_file.id, extension=guess_extension(tool_file.mimetype)) # check if file is image if "image" in mimetype: diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py index 3f844e8234..a3c84615ca 100644 --- a/api/core/tools/utils/parser.py +++ b/api/core/tools/utils/parser.py @@ -1,5 +1,4 @@ import re -import uuid from json import dumps as json_dumps from json import loads as json_loads from json.decoder import JSONDecodeError @@ -154,7 +153,7 @@ class ApiBasedToolSchemaParser: # remove special characters like / to ensure the operation id is valid ^[a-zA-Z0-9_-]{1,64}$ path = re.sub(r"[^a-zA-Z0-9_-]", "", path) if not path: - path = str(uuid.uuid4()) + path = "" interface["operation"]["operationId"] = f"{path}_{interface['method']}" diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 57c93d1d45..10bf8ca640 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -8,7 +8,12 @@ from flask_login import current_user from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime -from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolParameter, ToolProviderType +from core.tools.entities.tool_entities import ( + ToolEntity, + ToolInvokeMessage, + ToolParameter, + ToolProviderType, +) from core.tools.errors import ToolInvokeError from extensions.ext_database import db from factories.file_factory import build_from_mapping diff --git a/api/core/variables/segments.py b/api/core/variables/segments.py index 64ba16c367..13274f4e0e 100644 --- a/api/core/variables/segments.py +++ b/api/core/variables/segments.py @@ -1,9 +1,9 @@ import json import sys from collections.abc import Mapping, Sequence -from typing import Any +from typing import Annotated, Any, TypeAlias -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, ConfigDict, Discriminator, Tag, field_validator from core.file import File @@ -11,6 +11,11 @@ from .types import SegmentType class Segment(BaseModel): + """Segment is runtime type used during the execution of workflow. + + Note: this class is abstract, you should use subclasses of this class instead. + """ + model_config = ConfigDict(frozen=True) value_type: SegmentType @@ -73,12 +78,26 @@ class StringSegment(Segment): class FloatSegment(Segment): - value_type: SegmentType = SegmentType.NUMBER + value_type: SegmentType = SegmentType.FLOAT value: float + # NOTE(QuantumGhost): seems that the equality for FloatSegment with `NaN` value has some problems. + # The following tests cannot pass. + # + # def test_float_segment_and_nan(): + # nan = float("nan") + # assert nan != nan + # + # f1 = FloatSegment(value=float("nan")) + # f2 = FloatSegment(value=float("nan")) + # assert f1 != f2 + # + # f3 = FloatSegment(value=nan) + # f4 = FloatSegment(value=nan) + # assert f3 != f4 class IntegerSegment(Segment): - value_type: SegmentType = SegmentType.NUMBER + value_type: SegmentType = SegmentType.INTEGER value: int @@ -167,3 +186,46 @@ class ArrayFileSegment(ArraySegment): @property def text(self) -> str: return "" + + +def get_segment_discriminator(v: Any) -> SegmentType | None: + if isinstance(v, Segment): + return v.value_type + elif isinstance(v, dict): + value_type = v.get("value_type") + if value_type is None: + return None + try: + seg_type = SegmentType(value_type) + except ValueError: + return None + return seg_type + else: + # return None if the discriminator value isn't found + return None + + +# The `SegmentUnion`` type is used to enable serialization and deserialization with Pydantic. +# Use `Segment` for type hinting when serialization is not required. +# +# Note: +# - All variants in `SegmentUnion` must inherit from the `Segment` class. +# - The union must include all non-abstract subclasses of `Segment`, except: +# - `SegmentGroup`, which is not added to the variable pool. +# - `Variable` and its subclasses, which are handled by `VariableUnion`. +SegmentUnion: TypeAlias = Annotated[ + ( + Annotated[NoneSegment, Tag(SegmentType.NONE)] + | Annotated[StringSegment, Tag(SegmentType.STRING)] + | Annotated[FloatSegment, Tag(SegmentType.FLOAT)] + | Annotated[IntegerSegment, Tag(SegmentType.INTEGER)] + | Annotated[ObjectSegment, Tag(SegmentType.OBJECT)] + | Annotated[FileSegment, Tag(SegmentType.FILE)] + | Annotated[ArrayAnySegment, Tag(SegmentType.ARRAY_ANY)] + | Annotated[ArrayStringSegment, Tag(SegmentType.ARRAY_STRING)] + | Annotated[ArrayNumberSegment, Tag(SegmentType.ARRAY_NUMBER)] + | Annotated[ArrayObjectSegment, Tag(SegmentType.ARRAY_OBJECT)] + | Annotated[ArrayFileSegment, Tag(SegmentType.ARRAY_FILE)] + ), + Discriminator(get_segment_discriminator), +] diff --git a/api/core/variables/types.py b/api/core/variables/types.py index 4387e9693e..e39237dba5 100644 --- a/api/core/variables/types.py +++ b/api/core/variables/types.py @@ -1,8 +1,27 @@ +from collections.abc import Mapping from enum import StrEnum +from typing import Any, Optional + +from core.file.models import File + + +class ArrayValidation(StrEnum): + """Strategy for validating array elements""" + + # Skip element validation (only check array container) + NONE = "none" + + # Validate the first element (if array is non-empty) + FIRST = "first" + + # Validate all elements in the array. + ALL = "all" class SegmentType(StrEnum): NUMBER = "number" + INTEGER = "integer" + FLOAT = "float" STRING = "string" OBJECT = "object" SECRET = "secret" @@ -18,3 +37,142 @@ class SegmentType(StrEnum): NONE = "none" GROUP = "group" + + def is_array_type(self) -> bool: + return self in _ARRAY_TYPES + + @classmethod + def infer_segment_type(cls, value: Any) -> Optional["SegmentType"]: + """ + Attempt to infer the `SegmentType` based on the Python type of the `value` parameter. + + Returns `None` if no appropriate `SegmentType` can be determined for the given `value`. + For example, this may occur if the input is a generic Python object of type `object`. + """ + + if isinstance(value, list): + elem_types: set[SegmentType] = set() + for i in value: + segment_type = cls.infer_segment_type(i) + if segment_type is None: + return None + + elem_types.add(segment_type) + + if len(elem_types) != 1: + if elem_types.issubset(_NUMERICAL_TYPES): + return SegmentType.ARRAY_NUMBER + return SegmentType.ARRAY_ANY + elif all(i.is_array_type() for i in elem_types): + return SegmentType.ARRAY_ANY + match elem_types.pop(): + case SegmentType.STRING: + return SegmentType.ARRAY_STRING + case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: + return SegmentType.ARRAY_NUMBER + case SegmentType.OBJECT: + return SegmentType.ARRAY_OBJECT + case SegmentType.FILE: + return SegmentType.ARRAY_FILE + case SegmentType.NONE: + return SegmentType.ARRAY_ANY + case _: + # This should be unreachable. + raise ValueError(f"not supported value {value}") + if value is None: + return SegmentType.NONE + elif isinstance(value, int) and not isinstance(value, bool): + return SegmentType.INTEGER + elif isinstance(value, float): + return SegmentType.FLOAT + elif isinstance(value, str): + return SegmentType.STRING + elif isinstance(value, dict): + return SegmentType.OBJECT + elif isinstance(value, File): + return SegmentType.FILE + elif isinstance(value, str): + return SegmentType.STRING + else: + return None + + def _validate_array(self, value: Any, array_validation: ArrayValidation) -> bool: + if not isinstance(value, list): + return False + # Skip element validation if array is empty + if len(value) == 0: + return True + if self == SegmentType.ARRAY_ANY: + return True + element_type = _ARRAY_ELEMENT_TYPES_MAPPING[self] + + if array_validation == ArrayValidation.NONE: + return True + elif array_validation == ArrayValidation.FIRST: + return element_type.is_valid(value[0]) + else: + return all([element_type.is_valid(i, array_validation=ArrayValidation.NONE)] for i in value) + + def is_valid(self, value: Any, array_validation: ArrayValidation = ArrayValidation.FIRST) -> bool: + """ + Check if a value matches the segment type. + Users of `SegmentType` should call this method, instead of using + `isinstance` manually. + + Args: + value: The value to validate + array_validation: Validation strategy for array types (ignored for non-array types) + + Returns: + True if the value matches the type under the given validation strategy + """ + if self.is_array_type(): + return self._validate_array(value, array_validation) + elif self == SegmentType.NUMBER: + return isinstance(value, (int, float)) + elif self == SegmentType.STRING: + return isinstance(value, str) + elif self == SegmentType.OBJECT: + return isinstance(value, dict) + elif self == SegmentType.SECRET: + return isinstance(value, str) + elif self == SegmentType.FILE: + return isinstance(value, File) + elif self == SegmentType.NONE: + return value is None + else: + raise AssertionError("this statement should be unreachable.") + + def exposed_type(self) -> "SegmentType": + """Returns the type exposed to the frontend. + + The frontend treats `INTEGER` and `FLOAT` as `NUMBER`, so these are returned as `NUMBER` here. + """ + if self in (SegmentType.INTEGER, SegmentType.FLOAT): + return SegmentType.NUMBER + return self + + +_ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = { + # ARRAY_ANY does not have correpond element type. + SegmentType.ARRAY_STRING: SegmentType.STRING, + SegmentType.ARRAY_NUMBER: SegmentType.NUMBER, + SegmentType.ARRAY_OBJECT: SegmentType.OBJECT, + SegmentType.ARRAY_FILE: SegmentType.FILE, +} + +_ARRAY_TYPES = frozenset( + list(_ARRAY_ELEMENT_TYPES_MAPPING.keys()) + + [ + SegmentType.ARRAY_ANY, + ] +) + + +_NUMERICAL_TYPES = frozenset( + [ + SegmentType.NUMBER, + SegmentType.INTEGER, + SegmentType.FLOAT, + ] +) diff --git a/api/core/variables/utils.py b/api/core/variables/utils.py index e5d222af7d..692db3502e 100644 --- a/api/core/variables/utils.py +++ b/api/core/variables/utils.py @@ -1,8 +1,26 @@ +import json from collections.abc import Iterable, Sequence +from .segment_group import SegmentGroup +from .segments import ArrayFileSegment, FileSegment, Segment + def to_selector(node_id: str, name: str, paths: Iterable[str] = ()) -> Sequence[str]: selectors = [node_id, name] if paths: selectors.extend(paths) return selectors + + +class SegmentJSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ArrayFileSegment): + return [v.model_dump() for v in o.value] + elif isinstance(o, FileSegment): + return o.value.model_dump() + elif isinstance(o, SegmentGroup): + return [self.default(seg) for seg in o.value] + elif isinstance(o, Segment): + return o.value + else: + super().default(o) diff --git a/api/core/variables/variables.py b/api/core/variables/variables.py index b650b1682e..a31ebc848e 100644 --- a/api/core/variables/variables.py +++ b/api/core/variables/variables.py @@ -1,8 +1,8 @@ from collections.abc import Sequence -from typing import cast +from typing import Annotated, TypeAlias, cast from uuid import uuid4 -from pydantic import Field +from pydantic import Discriminator, Field, Tag from core.helper import encrypter @@ -20,6 +20,7 @@ from .segments import ( ObjectSegment, Segment, StringSegment, + get_segment_discriminator, ) from .types import SegmentType @@ -27,6 +28,10 @@ from .types import SegmentType class Variable(Segment): """ A variable is a segment that has a name. + + It is mainly used to store segments and their selector in VariablePool. + + Note: this class is abstract, you should use subclasses of this class instead. """ id: str = Field( @@ -93,3 +98,28 @@ class FileVariable(FileSegment, Variable): class ArrayFileVariable(ArrayFileSegment, ArrayVariable): pass + + +# The `VariableUnion`` type is used to enable serialization and deserialization with Pydantic. +# Use `Variable` for type hinting when serialization is not required. +# +# Note: +# - All variants in `VariableUnion` must inherit from the `Variable` class. +# - The union must include all non-abstract subclasses of `Segment`, except: +VariableUnion: TypeAlias = Annotated[ + ( + Annotated[NoneVariable, Tag(SegmentType.NONE)] + | Annotated[StringVariable, Tag(SegmentType.STRING)] + | Annotated[FloatVariable, Tag(SegmentType.FLOAT)] + | Annotated[IntegerVariable, Tag(SegmentType.INTEGER)] + | Annotated[ObjectVariable, Tag(SegmentType.OBJECT)] + | Annotated[FileVariable, Tag(SegmentType.FILE)] + | Annotated[ArrayAnyVariable, Tag(SegmentType.ARRAY_ANY)] + | Annotated[ArrayStringVariable, Tag(SegmentType.ARRAY_STRING)] + | Annotated[ArrayNumberVariable, Tag(SegmentType.ARRAY_NUMBER)] + | Annotated[ArrayObjectVariable, Tag(SegmentType.ARRAY_OBJECT)] + | Annotated[ArrayFileVariable, Tag(SegmentType.ARRAY_FILE)] + | Annotated[SecretVariable, Tag(SegmentType.SECRET)] + ), + Discriminator(get_segment_discriminator), +] diff --git a/api/core/workflow/callbacks/workflow_logging_callback.py b/api/core/workflow/callbacks/workflow_logging_callback.py index e6813a3997..12b5203ca3 100644 --- a/api/core/workflow/callbacks/workflow_logging_callback.py +++ b/api/core/workflow/callbacks/workflow_logging_callback.py @@ -232,14 +232,14 @@ class WorkflowLoggingCallback(WorkflowCallback): Publish loop started """ self.print_text("\n[LoopRunStartedEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def on_workflow_loop_next(self, event: LoopRunNextEvent) -> None: """ Publish loop next """ self.print_text("\n[LoopRunNextEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") self.print_text(f"Loop Index: {event.index}", color="blue") def on_workflow_loop_completed(self, event: LoopRunSucceededEvent | LoopRunFailedEvent) -> None: @@ -250,7 +250,7 @@ class WorkflowLoggingCallback(WorkflowCallback): "\n[LoopRunSucceededEvent]" if isinstance(event, LoopRunSucceededEvent) else "\n[LoopRunFailedEvent]", color="blue", ) - self.print_text(f"Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None: """Print text with highlighting and no end characters.""" diff --git a/api/core/workflow/conversation_variable_updater.py b/api/core/workflow/conversation_variable_updater.py new file mode 100644 index 0000000000..84e99bb582 --- /dev/null +++ b/api/core/workflow/conversation_variable_updater.py @@ -0,0 +1,39 @@ +import abc +from typing import Protocol + +from core.variables import Variable + + +class ConversationVariableUpdater(Protocol): + """ + ConversationVariableUpdater defines an abstraction for updating conversation variable values. + + It is intended for use by `v1.VariableAssignerNode` and `v2.VariableAssignerNode` when updating + conversation variables. + + Implementations may choose to batch updates. If batching is used, the `flush` method + should be implemented to persist buffered changes, and `update` + should handle buffering accordingly. + + Note: Since implementations may buffer updates, instances of ConversationVariableUpdater + are not thread-safe. Each VariableAssignerNode should create its own instance during execution. + """ + + @abc.abstractmethod + def update(self, conversation_id: str, variable: "Variable") -> None: + """ + Updates the value of the specified conversation variable in the underlying storage. + + :param conversation_id: The ID of the conversation to update. Typically references `ConversationVariable.id`. + :param variable: The `Variable` instance containing the updated value. + """ + pass + + @abc.abstractmethod + def flush(self): + """ + Flushes all pending updates to the underlying storage system. + + If the implementation does not buffer updates, this method can be a no-op. + """ + pass diff --git a/api/core/workflow/entities/variable_pool.py b/api/core/workflow/entities/variable_pool.py index af26864c01..646a9d3402 100644 --- a/api/core/workflow/entities/variable_pool.py +++ b/api/core/workflow/entities/variable_pool.py @@ -1,18 +1,19 @@ import re from collections import defaultdict from collections.abc import Mapping, Sequence -from typing import Any, Union +from typing import Annotated, Any, Union, cast from pydantic import BaseModel, Field from core.file import File, FileAttribute, file_manager from core.variables import Segment, SegmentGroup, Variable +from core.variables.consts import MIN_SELECTORS_LENGTH from core.variables.segments import FileSegment, NoneSegment +from core.variables.variables import VariableUnion +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from core.workflow.system_variable import SystemVariable from factories import variable_factory -from ..constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID -from ..enums import SystemVariableKey - VariableValue = Union[str, int, float, dict, list, File] VARIABLE_PATTERN = re.compile(r"\{\{#([a-zA-Z0-9_]{1,50}(?:\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10})#\}\}") @@ -23,50 +24,31 @@ class VariablePool(BaseModel): # The first element of the selector is the node id, it's the first-level key in the dictionary. # Other elements of the selector are the keys in the second-level dictionary. To get the key, we hash the # elements of the selector except the first one. - variable_dictionary: dict[str, dict[int, Segment]] = Field( + variable_dictionary: defaultdict[str, Annotated[dict[int, VariableUnion], Field(default_factory=dict)]] = Field( description="Variables mapping", default=defaultdict(dict), ) - # TODO: This user inputs is not used for pool. + + # The `user_inputs` is used only when constructing the inputs for the `StartNode`. It's not used elsewhere. user_inputs: Mapping[str, Any] = Field( description="User inputs", + default_factory=dict, ) - system_variables: Mapping[SystemVariableKey, Any] = Field( + system_variables: SystemVariable = Field( description="System variables", ) - environment_variables: Sequence[Variable] = Field( + environment_variables: Sequence[VariableUnion] = Field( description="Environment variables.", default_factory=list, ) - conversation_variables: Sequence[Variable] = Field( + conversation_variables: Sequence[VariableUnion] = Field( description="Conversation variables.", default_factory=list, ) - def __init__( - self, - *, - system_variables: Mapping[SystemVariableKey, Any] | None = None, - user_inputs: Mapping[str, Any] | None = None, - environment_variables: Sequence[Variable] | None = None, - conversation_variables: Sequence[Variable] | None = None, - **kwargs, - ): - environment_variables = environment_variables or [] - conversation_variables = conversation_variables or [] - user_inputs = user_inputs or {} - system_variables = system_variables or {} - - super().__init__( - system_variables=system_variables, - user_inputs=user_inputs, - environment_variables=environment_variables, - conversation_variables=conversation_variables, - **kwargs, - ) - - for key, value in self.system_variables.items(): - self.add((SYSTEM_VARIABLE_NODE_ID, key.value), value) + def model_post_init(self, context: Any, /) -> None: + # Create a mapping from field names to SystemVariableKey enum values + self._add_system_variables(self.system_variables) # Add environment variables to the variable pool for var in self.environment_variables: self.add((ENVIRONMENT_VARIABLE_NODE_ID, var.name), var) @@ -91,19 +73,33 @@ class VariablePool(BaseModel): Returns: None """ - if len(selector) < 2: + if len(selector) < MIN_SELECTORS_LENGTH: raise ValueError("Invalid selector") if isinstance(value, Variable): variable = value - if isinstance(value, Segment): + elif isinstance(value, Segment): variable = variable_factory.segment_to_variable(segment=value, selector=selector) else: segment = variable_factory.build_segment(value) variable = variable_factory.segment_to_variable(segment=segment, selector=selector) - hash_key = hash(tuple(selector[1:])) - self.variable_dictionary[selector[0]][hash_key] = variable + key, hash_key = self._selector_to_keys(selector) + # Based on the definition of `VariableUnion`, + # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. + self.variable_dictionary[key][hash_key] = cast(VariableUnion, variable) + + @classmethod + def _selector_to_keys(cls, selector: Sequence[str]) -> tuple[str, int]: + return selector[0], hash(tuple(selector[1:])) + + def _has(self, selector: Sequence[str]) -> bool: + key, hash_key = self._selector_to_keys(selector) + if key not in self.variable_dictionary: + return False + if hash_key not in self.variable_dictionary[key]: + return False + return True def get(self, selector: Sequence[str], /) -> Segment | None: """ @@ -118,11 +114,11 @@ class VariablePool(BaseModel): Raises: ValueError: If the selector is invalid. """ - if len(selector) < 2: + if len(selector) < MIN_SELECTORS_LENGTH: return None - hash_key = hash(tuple(selector[1:])) - value = self.variable_dictionary[selector[0]].get(hash_key) + key, hash_key = self._selector_to_keys(selector) + value: Segment | None = self.variable_dictionary[key].get(hash_key) if value is None: selector, attr = selector[:-1], selector[-1] @@ -155,8 +151,9 @@ class VariablePool(BaseModel): if len(selector) == 1: self.variable_dictionary[selector[0]] = {} return + key, hash_key = self._selector_to_keys(selector) hash_key = hash(tuple(selector[1:])) - self.variable_dictionary[selector[0]].pop(hash_key, None) + self.variable_dictionary[key].pop(hash_key, None) def convert_template(self, template: str, /): parts = VARIABLE_PATTERN.split(template) @@ -173,3 +170,20 @@ class VariablePool(BaseModel): if isinstance(segment, FileSegment): return segment return None + + def _add_system_variables(self, system_variable: SystemVariable): + sys_var_mapping = system_variable.to_dict() + for key, value in sys_var_mapping.items(): + if value is None: + continue + selector = (SYSTEM_VARIABLE_NODE_ID, key) + # If the system variable already exists, do not add it again. + # This ensures that we can keep the id of the system variables intact. + if self._has(selector): + continue + self.add(selector, value) # type: ignore + + @classmethod + def empty(cls) -> "VariablePool": + """Create an empty variable pool.""" + return cls(system_variables=SystemVariable.empty()) diff --git a/api/core/workflow/entities/workflow_node_execution.py b/api/core/workflow/entities/workflow_node_execution.py index 773f5b777b..09a408f4d7 100644 --- a/api/core/workflow/entities/workflow_node_execution.py +++ b/api/core/workflow/entities/workflow_node_execution.py @@ -66,11 +66,21 @@ class WorkflowNodeExecution(BaseModel): but they are not stored in the model. """ - # Core identification fields - id: str # Unique identifier for this execution record - node_execution_id: Optional[str] = None # Optional secondary ID for cross-referencing + # --------- Core identification fields --------- + + # Unique identifier for this execution record, used when persisting to storage. + # Value is a UUID string (e.g., '09b3e04c-f9ae-404c-ad82-290b8d7bd382'). + id: str + + # Optional secondary ID for cross-referencing purposes. + # + # NOTE: For referencing the persisted record, use `id` rather than `node_execution_id`. + # While `node_execution_id` may sometimes be a UUID string, this is not guaranteed. + # In most scenarios, `id` should be used as the primary identifier. + node_execution_id: Optional[str] = None workflow_id: str # ID of the workflow this node belongs to workflow_execution_id: Optional[str] = None # ID of the specific workflow run (null for single-step debugging) + # --------- Core identification fields ends --------- # Execution positioning and flow index: int # Sequence number for ordering in trace visualization diff --git a/api/core/workflow/graph_engine/entities/event.py b/api/core/workflow/graph_engine/entities/event.py index 9a4939502e..e57e9e4d64 100644 --- a/api/core/workflow/graph_engine/entities/event.py +++ b/api/core/workflow/graph_engine/entities/event.py @@ -66,6 +66,8 @@ class BaseNodeEvent(GraphEngineEvent): """iteration id if node is in iteration""" in_loop_id: Optional[str] = None """loop id if node is in loop""" + # The version of the node, or "1" if not specified. + node_version: str = "1" class NodeRunStartedEvent(BaseNodeEvent): diff --git a/api/core/workflow/graph_engine/entities/graph.py b/api/core/workflow/graph_engine/entities/graph.py index 8e5b1e7142..362777a199 100644 --- a/api/core/workflow/graph_engine/entities/graph.py +++ b/api/core/workflow/graph_engine/entities/graph.py @@ -334,7 +334,7 @@ class Graph(BaseModel): parallel = GraphParallel( start_from_node_id=start_node_id, - parent_parallel_id=parent_parallel.id if parent_parallel else None, + parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None, ) parallel_mapping[parallel.id] = parallel diff --git a/api/core/workflow/graph_engine/entities/graph_runtime_state.py b/api/core/workflow/graph_engine/entities/graph_runtime_state.py index afc09bfac5..a62ffe46c9 100644 --- a/api/core/workflow/graph_engine/entities/graph_runtime_state.py +++ b/api/core/workflow/graph_engine/entities/graph_runtime_state.py @@ -17,8 +17,12 @@ class GraphRuntimeState(BaseModel): """total tokens""" llm_usage: LLMUsage = LLMUsage.empty_usage() """llm usage info""" + + # The `outputs` field stores the final output values generated by executing workflows or chatflows. + # + # Note: Since the type of this field is `dict[str, Any]`, its values may not remain consistent + # after a serialization and deserialization round trip. outputs: dict[str, Any] = {} - """outputs""" node_run_steps: int = 0 """node run steps""" diff --git a/api/core/workflow/graph_engine/graph_engine.py b/api/core/workflow/graph_engine/graph_engine.py index 363b2ee920..5a2915e2d3 100644 --- a/api/core/workflow/graph_engine/graph_engine.py +++ b/api/core/workflow/graph_engine/graph_engine.py @@ -9,7 +9,7 @@ from copy import copy, deepcopy from datetime import UTC, datetime from typing import Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.app.apps.base_app_queue_manager import GenerateTaskStoppedError @@ -53,6 +53,8 @@ from core.workflow.nodes.end.end_stream_processor import EndStreamProcessor from core.workflow.nodes.enums import ErrorStrategy, FailBranchSourceHandle from core.workflow.nodes.event import RunCompletedEvent, RunRetrieverResourceEvent, RunStreamChunkEvent from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING +from core.workflow.utils import variable_utils +from libs.flask_utils import preserve_flask_contexts from models.enums import UserFrom from models.workflow import WorkflowType @@ -101,7 +103,7 @@ class GraphEngine: call_depth: int, graph: Graph, graph_config: Mapping[str, Any], - variable_pool: VariablePool, + graph_runtime_state: GraphRuntimeState, max_execution_steps: int, max_execution_time: int, thread_pool_id: Optional[str] = None, @@ -138,7 +140,7 @@ class GraphEngine: call_depth=call_depth, ) - self.graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) + self.graph_runtime_state = graph_runtime_state self.max_execution_steps = max_execution_steps self.max_execution_time = max_execution_time @@ -313,6 +315,7 @@ class GraphEngine: parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) raise e @@ -537,24 +540,9 @@ class GraphEngine: """ Run parallel nodes """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): + with preserve_flask_contexts(flask_app, context_vars=context): try: - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user - q.put( ParallelBranchRunStartedEvent( parallel_id=parallel_id, @@ -641,6 +629,7 @@ class GraphEngine: parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, agent_strategy=agent_strategy, + node_version=node_instance.version(), ) max_retries = node_instance.node_data.retry_config.max_retries @@ -653,26 +642,19 @@ class GraphEngine: retry_start_at = datetime.now(UTC).replace(tzinfo=None) # yield control to other threads time.sleep(0.001) - generator = node_instance.run() - for item in generator: - if isinstance(item, GraphEngineEvent): - if isinstance(item, BaseIterationEvent): - # add parallel info to iteration event - item.parallel_id = parallel_id - item.parallel_start_node_id = parallel_start_node_id - item.parent_parallel_id = parent_parallel_id - item.parent_parallel_start_node_id = parent_parallel_start_node_id - elif isinstance(item, BaseLoopEvent): - # add parallel info to loop event - item.parallel_id = parallel_id - item.parallel_start_node_id = parallel_start_node_id - item.parent_parallel_id = parent_parallel_id - item.parent_parallel_start_node_id = parent_parallel_start_node_id - - yield item + event_stream = node_instance.run() + for event in event_stream: + if isinstance(event, GraphEngineEvent): + # add parallel info to iteration event + if isinstance(event, BaseIterationEvent | BaseLoopEvent): + event.parallel_id = parallel_id + event.parallel_start_node_id = parallel_start_node_id + event.parent_parallel_id = parent_parallel_id + event.parent_parallel_start_node_id = parent_parallel_start_node_id + yield event else: - if isinstance(item, RunCompletedEvent): - run_result = item.run_result + if isinstance(event, RunCompletedEvent): + run_result = event.run_result if run_result.status == WorkflowNodeExecutionStatus.FAILED: if ( retries == max_retries @@ -698,6 +680,7 @@ class GraphEngine: error=run_result.error or "Unknown error", retry_index=retries, start_at=retry_start_at, + node_version=node_instance.version(), ) time.sleep(retry_interval) break @@ -708,7 +691,7 @@ class GraphEngine: # if run failed, handle error run_result = self._handle_continue_on_error( node_instance, - item.run_result, + event.run_result, self.graph_runtime_state.variable_pool, handle_exceptions=handle_exceptions, ) @@ -733,6 +716,7 @@ class GraphEngine: parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) should_continue_retry = False else: @@ -747,6 +731,7 @@ class GraphEngine: parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) should_continue_retry = False elif run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED: @@ -807,37 +792,40 @@ class GraphEngine: parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) should_continue_retry = False break - elif isinstance(item, RunStreamChunkEvent): + elif isinstance(event, RunStreamChunkEvent): yield NodeRunStreamChunkEvent( id=node_instance.id, node_id=node_instance.node_id, node_type=node_instance.node_type, node_data=node_instance.node_data, - chunk_content=item.chunk_content, - from_variable_selector=item.from_variable_selector, + chunk_content=event.chunk_content, + from_variable_selector=event.from_variable_selector, route_node_state=route_node_state, parallel_id=parallel_id, parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) - elif isinstance(item, RunRetrieverResourceEvent): + elif isinstance(event, RunRetrieverResourceEvent): yield NodeRunRetrieverResourceEvent( id=node_instance.id, node_id=node_instance.node_id, node_type=node_instance.node_type, node_data=node_instance.node_data, - retriever_resources=item.retriever_resources, - context=item.context, + retriever_resources=event.retriever_resources, + context=event.context, route_node_state=route_node_state, parallel_id=parallel_id, parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) except GenerateTaskStoppedError: # trigger node run failed event @@ -854,6 +842,7 @@ class GraphEngine: parallel_start_node_id=parallel_start_node_id, parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel_start_node_id, + node_version=node_instance.version(), ) return except Exception as e: @@ -868,16 +857,12 @@ class GraphEngine: :param variable_value: variable value :return: """ - self.graph_runtime_state.variable_pool.add([node_id] + variable_key_list, variable_value) - - # if variable_value is a dict, then recursively append variables - if isinstance(variable_value, dict): - for key, value in variable_value.items(): - # construct new key list - new_key_list = variable_key_list + [key] - self._append_variables_recursively( - node_id=node_id, variable_key_list=new_key_list, variable_value=value - ) + variable_utils.append_variables_recursively( + self.graph_runtime_state.variable_pool, + node_id, + variable_key_list, + variable_value, + ) def _is_timed_out(self, start_at: float, max_execution_time: int) -> bool: """ diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index faa8f90bea..678b99d546 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -1,19 +1,22 @@ import json +import uuid from collections.abc import Generator, Mapping, Sequence from typing import Any, Optional, cast +from packaging.version import Version from sqlalchemy import select from sqlalchemy.orm import Session from core.agent.entities import AgentToolEntity from core.agent.plugin_entities import AgentStrategyParameter +from core.agent.strategy.plugin import PluginAgentStrategy from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance, ModelManager from core.model_runtime.entities.model_entities import AIModelEntity, ModelType from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.plugin import PluginInstaller from core.provider_manager import ProviderManager -from core.tools.entities.tool_entities import ToolParameter, ToolProviderType +from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter, ToolProviderType from core.tools.tool_manager import ToolManager from core.variables.segments import StringSegment from core.workflow.entities.node_entities import NodeRunResult @@ -39,6 +42,10 @@ class AgentNode(ToolNode): _node_data_cls = AgentNodeData # type: ignore _node_type = NodeType.AGENT + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> Generator: """ Run the agent node @@ -68,12 +75,14 @@ class AgentNode(ToolNode): agent_parameters=agent_parameters, variable_pool=self.graph_runtime_state.variable_pool, node_data=node_data, + strategy=strategy, ) parameters_for_log = self._generate_agent_parameters( agent_parameters=agent_parameters, variable_pool=self.graph_runtime_state.variable_pool, node_data=node_data, for_log=True, + strategy=strategy, ) # get conversation id @@ -98,6 +107,32 @@ class AgentNode(ToolNode): try: # convert tool messages + agent_thoughts: list = [] + + thought_log_message = ToolInvokeMessage( + type=ToolInvokeMessage.MessageType.LOG, + message=ToolInvokeMessage.LogMessage( + id=str(uuid.uuid4()), + label=f"Agent Strategy: {cast(AgentNodeData, self.node_data).agent_strategy_name}", + parent_id=None, + error=None, + status=ToolInvokeMessage.LogMessage.LogStatus.START, + data={ + "strategy": cast(AgentNodeData, self.node_data).agent_strategy_name, + "parameters": parameters_for_log, + "thought_process": "Agent strategy execution started", + }, + metadata={ + "icon": self.agent_strategy_icon, + "agent_strategy": cast(AgentNodeData, self.node_data).agent_strategy_name, + }, + ), + ) + + def enhanced_message_stream(): + yield thought_log_message + + yield from message_stream yield from self._transform_message( message_stream, @@ -106,6 +141,7 @@ class AgentNode(ToolNode): "agent_strategy": cast(AgentNodeData, self.node_data).agent_strategy_name, }, parameters_for_log, + agent_thoughts, ) except PluginDaemonClientSideError as e: yield RunCompletedEvent( @@ -123,6 +159,7 @@ class AgentNode(ToolNode): variable_pool: VariablePool, node_data: AgentNodeData, for_log: bool = False, + strategy: PluginAgentStrategy, ) -> dict[str, Any]: """ Generate parameters based on the given tool parameters, variable pool, and node data. @@ -154,7 +191,10 @@ class AgentNode(ToolNode): # variable_pool.convert_template expects a string template, # but if passing a dict, convert to JSON string first before rendering try: - parameter_value = json.dumps(agent_input.value, ensure_ascii=False) + if not isinstance(agent_input.value, str): + parameter_value = json.dumps(agent_input.value, ensure_ascii=False) + else: + parameter_value = str(agent_input.value) except TypeError: parameter_value = str(agent_input.value) segment_group = variable_pool.convert_template(parameter_value) @@ -162,7 +202,8 @@ class AgentNode(ToolNode): # variable_pool.convert_template returns a string, # so we need to convert it back to a dictionary try: - parameter_value = json.loads(parameter_value) + if not isinstance(agent_input.value, str): + parameter_value = json.loads(parameter_value) except json.JSONDecodeError: parameter_value = parameter_value else: @@ -171,7 +212,7 @@ class AgentNode(ToolNode): if parameter.type == "array[tools]": value = cast(list[dict[str, Any]], value) value = [tool for tool in value if tool.get("enabled", False)] - + value = self._filter_mcp_type_tool(strategy, value) for tool in value: if "schemas" in tool: tool.pop("schemas") @@ -208,13 +249,13 @@ class AgentNode(ToolNode): ) extra = tool.get("extra", {}) - + runtime_variable_pool = variable_pool if self.node_data.version != "1" else None tool_runtime = ToolManager.get_agent_tool_runtime( - self.tenant_id, self.app_id, entity, self.invoke_from + self.tenant_id, self.app_id, entity, self.invoke_from, runtime_variable_pool ) if tool_runtime.entity.description: tool_runtime.entity.description.llm = ( - extra.get("descrption", "") or tool_runtime.entity.description.llm + extra.get("description", "") or tool_runtime.entity.description.llm ) for tool_runtime_params in tool_runtime.entity.parameters: tool_runtime_params.form = ( @@ -362,3 +403,16 @@ class AgentNode(ToolNode): except ValueError: model_schema.features.remove(feature) return model_schema + + def _filter_mcp_type_tool(self, strategy: PluginAgentStrategy, tools: list[dict[str, Any]]) -> list[dict[str, Any]]: + """ + Filter MCP type tool + :param strategy: plugin agent strategy + :param tool: tool + :return: filtered tool dict + """ + meta_version = strategy.meta_version + if meta_version and Version(meta_version) > Version("0.0.1"): + return tools + else: + return [tool for tool in tools if tool.get("type") != ToolProviderType.MCP.value] diff --git a/api/core/workflow/nodes/answer/answer_node.py b/api/core/workflow/nodes/answer/answer_node.py index aa030870e2..38c2bcbdf5 100644 --- a/api/core/workflow/nodes/answer/answer_node.py +++ b/api/core/workflow/nodes/answer/answer_node.py @@ -18,7 +18,11 @@ from core.workflow.utils.variable_template_parser import VariableTemplateParser class AnswerNode(BaseNode[AnswerNodeData]): _node_data_cls = AnswerNodeData - _node_type: NodeType = NodeType.ANSWER + _node_type = NodeType.ANSWER + + @classmethod + def version(cls) -> str: + return "1" def _run(self) -> NodeRunResult: """ @@ -45,7 +49,10 @@ class AnswerNode(BaseNode[AnswerNodeData]): part = cast(TextGenerateRouteChunk, part) answer += part.text - return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={"answer": answer, "files": files}) + return NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + outputs={"answer": answer, "files": ArrayFileSegment(value=files)}, + ) @classmethod def _extract_variable_selector_to_variable_mapping( diff --git a/api/core/workflow/nodes/answer/answer_stream_processor.py b/api/core/workflow/nodes/answer/answer_stream_processor.py index ba6ba16e36..97666fad05 100644 --- a/api/core/workflow/nodes/answer/answer_stream_processor.py +++ b/api/core/workflow/nodes/answer/answer_stream_processor.py @@ -2,7 +2,6 @@ import logging from collections.abc import Generator from typing import cast -from core.file import FILE_MODEL_IDENTITY, File from core.workflow.entities.variable_pool import VariablePool from core.workflow.graph_engine.entities.event import ( GraphEngineEvent, @@ -109,6 +108,7 @@ class AnswerStreamProcessor(StreamProcessor): parallel_id=event.parallel_id, parallel_start_node_id=event.parallel_start_node_id, from_variable_selector=[answer_node_id, "answer"], + node_version=event.node_version, ) else: route_chunk = cast(VarGenerateRouteChunk, route_chunk) @@ -134,6 +134,7 @@ class AnswerStreamProcessor(StreamProcessor): route_node_state=event.route_node_state, parallel_id=event.parallel_id, parallel_start_node_id=event.parallel_start_node_id, + node_version=event.node_version, ) self.route_position[answer_node_id] += 1 @@ -199,44 +200,3 @@ class AnswerStreamProcessor(StreamProcessor): stream_out_answer_node_ids.append(answer_node_id) return stream_out_answer_node_ids - - @classmethod - def _fetch_files_from_variable_value(cls, value: dict | list) -> list[dict]: - """ - Fetch files from variable value - :param value: variable value - :return: - """ - if not value: - return [] - - files = [] - if isinstance(value, list): - for item in value: - file_var = cls._get_file_var_from_value(item) - if file_var: - files.append(file_var) - elif isinstance(value, dict): - file_var = cls._get_file_var_from_value(value) - if file_var: - files.append(file_var) - - return files - - @classmethod - def _get_file_var_from_value(cls, value: dict | list): - """ - Get file var from value - :param value: variable value - :return: - """ - if not value: - return None - - if isinstance(value, dict): - if "dify_model_identity" in value and value["dify_model_identity"] == FILE_MODEL_IDENTITY: - return value - elif isinstance(value, File): - return value.to_dict() - - return None diff --git a/api/core/workflow/nodes/answer/base_stream_processor.py b/api/core/workflow/nodes/answer/base_stream_processor.py index 6671ff0746..09d5464d7a 100644 --- a/api/core/workflow/nodes/answer/base_stream_processor.py +++ b/api/core/workflow/nodes/answer/base_stream_processor.py @@ -57,7 +57,6 @@ class StreamProcessor(ABC): # The branch_identify parameter is added to ensure that # only nodes in the correct logical branch are included. - reachable_node_ids.append(edge.target_node_id) ids = self._fetch_node_ids_in_reachable_branch(edge.target_node_id, run_result.edge_source_handle) reachable_node_ids.extend(ids) else: @@ -74,6 +73,8 @@ class StreamProcessor(ABC): self._remove_node_ids_in_unreachable_branch(node_id, reachable_node_ids) def _fetch_node_ids_in_reachable_branch(self, node_id: str, branch_identify: Optional[str] = None) -> list[str]: + if node_id not in self.rest_node_ids: + self.rest_node_ids.append(node_id) node_ids = [] for edge in self.graph.edge_mapping.get(node_id, []): if edge.target_node_id == self.graph.root_node_id: diff --git a/api/core/workflow/nodes/base/node.py b/api/core/workflow/nodes/base/node.py index 7da0c19740..6973401429 100644 --- a/api/core/workflow/nodes/base/node.py +++ b/api/core/workflow/nodes/base/node.py @@ -1,7 +1,7 @@ import logging from abc import abstractmethod from collections.abc import Generator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, ClassVar, Generic, Optional, TypeVar, Union, cast from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus @@ -23,7 +23,7 @@ GenericNodeData = TypeVar("GenericNodeData", bound=BaseNodeData) class BaseNode(Generic[GenericNodeData]): _node_data_cls: type[GenericNodeData] - _node_type: NodeType + _node_type: ClassVar[NodeType] def __init__( self, @@ -90,8 +90,38 @@ class BaseNode(Generic[GenericNodeData]): graph_config: Mapping[str, Any], config: Mapping[str, Any], ) -> Mapping[str, Sequence[str]]: - """ - Extract variable selector to variable mapping + """Extracts references variable selectors from node configuration. + + The `config` parameter represents the configuration for a specific node type and corresponds + to the `data` field in the node definition object. + + The returned mapping has the following structure: + + {'1747829548239.#1747829667553.result#': ['1747829667553', 'result']} + + For loop and iteration nodes, the mapping may look like this: + + { + "1748332301644.input_selector": ["1748332363630", "result"], + "1748332325079.1748332325079.#sys.workflow_id#": ["sys", "workflow_id"], + } + + where `1748332301644` is the ID of the loop / iteration node, + and `1748332325079` is the ID of the node inside the loop or iteration node. + + Here, the key consists of two parts: the current node ID (provided as the `node_id` + parameter to `_extract_variable_selector_to_variable_mapping`) and the variable selector, + enclosed in `#` symbols. These two parts are separated by a dot (`.`). + + The value is a list of string representing the variable selector, where the first element is the node ID + of the referenced variable, and the second element is the variable name within that node. + + The meaning of the above response is: + + The node with ID `1747829548239` references the variable `result` from the node with + ID `1747829667553`. For example, if `1747829548239` is a LLM node, its prompt may contain a + reference to the `result` output variable of node `1747829667553`. + :param graph_config: graph config :param config: node config :return: @@ -101,9 +131,10 @@ class BaseNode(Generic[GenericNodeData]): raise ValueError("Node ID is required when extracting variable selector to variable mapping.") node_data = cls._node_data_cls(**config.get("data", {})) - return cls._extract_variable_selector_to_variable_mapping( + data = cls._extract_variable_selector_to_variable_mapping( graph_config=graph_config, node_id=node_id, node_data=cast(GenericNodeData, node_data) ) + return data @classmethod def _extract_variable_selector_to_variable_mapping( @@ -139,6 +170,16 @@ class BaseNode(Generic[GenericNodeData]): """ return self._node_type + @classmethod + @abstractmethod + def version(cls) -> str: + """`node_version` returns the version of current node type.""" + # NOTE(QuantumGhost): This should be in sync with `NODE_TYPE_CLASSES_MAPPING`. + # + # If you have introduced a new node type, please add it to `NODE_TYPE_CLASSES_MAPPING` + # in `api/core/workflow/nodes/__init__.py`. + raise NotImplementedError("subclasses of BaseNode must implement `version` method.") + @property def should_continue_on_error(self) -> bool: """judge if should continue on error diff --git a/api/core/workflow/nodes/code/code_node.py b/api/core/workflow/nodes/code/code_node.py index 61c08a7d71..22ed9e2651 100644 --- a/api/core/workflow/nodes/code/code_node.py +++ b/api/core/workflow/nodes/code/code_node.py @@ -40,6 +40,10 @@ class CodeNode(BaseNode[CodeNodeData]): return code_provider.get_default_config() + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: # Get code language code_language = self.node_data.code_language @@ -126,6 +130,9 @@ class CodeNode(BaseNode[CodeNodeData]): prefix: str = "", depth: int = 1, ): + # TODO(QuantumGhost): Replace native Python lists with `Array*Segment` classes. + # Note that `_transform_result` may produce lists containing `None` values, + # which don't conform to the type requirements of `Array*Segment` classes. if depth > dify_config.CODE_MAX_DEPTH: raise DepthLimitError(f"Depth limit {dify_config.CODE_MAX_DEPTH} reached, object too deep.") diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index 429fed2d04..8e6150f9cc 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -24,7 +24,7 @@ from configs import dify_config from core.file import File, FileTransferMethod, file_manager from core.helper import ssrf_proxy from core.variables import ArrayFileSegment -from core.variables.segments import FileSegment +from core.variables.segments import ArrayStringSegment, FileSegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.base import BaseNode @@ -45,6 +45,10 @@ class DocumentExtractorNode(BaseNode[DocumentExtractorNodeData]): _node_data_cls = DocumentExtractorNodeData _node_type = NodeType.DOCUMENT_EXTRACTOR + @classmethod + def version(cls) -> str: + return "1" + def _run(self): variable_selector = self.node_data.variable_selector variable = self.graph_runtime_state.variable_pool.get(variable_selector) @@ -67,7 +71,7 @@ class DocumentExtractorNode(BaseNode[DocumentExtractorNodeData]): status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=inputs, process_data=process_data, - outputs={"text": extracted_text_list}, + outputs={"text": ArrayStringSegment(value=extracted_text_list)}, ) elif isinstance(value, File): extracted_text = _extract_text_from_file(value) @@ -447,7 +451,7 @@ def _extract_text_from_excel(file_content: bytes) -> str: df = df.applymap(lambda x: " ".join(str(x).splitlines()) if isinstance(x, str) else x) # type: ignore # Combine multi-line text in column names into a single line - df.columns = pd.Index([" ".join(col.splitlines()) for col in df.columns]) + df.columns = pd.Index([" ".join(str(col).splitlines()) for col in df.columns]) # Manually construct the Markdown table markdown_table += _construct_markdown_table(df) + "\n\n" diff --git a/api/core/workflow/nodes/end/end_node.py b/api/core/workflow/nodes/end/end_node.py index 0e9756b243..17a0b3adeb 100644 --- a/api/core/workflow/nodes/end/end_node.py +++ b/api/core/workflow/nodes/end/end_node.py @@ -9,6 +9,10 @@ class EndNode(BaseNode[EndNodeData]): _node_data_cls = EndNodeData _node_type = NodeType.END + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: """ Run node diff --git a/api/core/workflow/nodes/end/end_stream_processor.py b/api/core/workflow/nodes/end/end_stream_processor.py index 3ae5af7137..a6fb2ffc18 100644 --- a/api/core/workflow/nodes/end/end_stream_processor.py +++ b/api/core/workflow/nodes/end/end_stream_processor.py @@ -139,6 +139,7 @@ class EndStreamProcessor(StreamProcessor): route_node_state=event.route_node_state, parallel_id=event.parallel_id, parallel_start_node_id=event.parallel_start_node_id, + node_version=event.node_version, ) self.route_position[end_node_id] += 1 diff --git a/api/core/workflow/nodes/event/event.py b/api/core/workflow/nodes/event/event.py index b72d111f49..3ebe80f245 100644 --- a/api/core/workflow/nodes/event/event.py +++ b/api/core/workflow/nodes/event/event.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, Field from core.model_runtime.entities.llm_entities import LLMUsage from core.rag.entities.citation_metadata import RetrievalSourceMetadata from core.workflow.entities.node_entities import NodeRunResult -from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus class RunCompletedEvent(BaseModel): @@ -39,11 +38,3 @@ class RunRetryEvent(BaseModel): error: str = Field(..., description="error") retry_index: int = Field(..., description="Retry attempt number") start_at: datetime = Field(..., description="Retry start time") - - -class SingleStepRetryEvent(NodeRunResult): - """Single step retry event""" - - status: WorkflowNodeExecutionStatus = WorkflowNodeExecutionStatus.RETRY - - elapsed_time: float = Field(..., description="elapsed time") diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index e28ac6343b..8ac1ae8526 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -1,12 +1,14 @@ import base64 import json +import secrets +import string from collections.abc import Mapping from copy import deepcopy -from random import randint from typing import Any, Literal from urllib.parse import urlencode, urlparse import httpx +from json_repair import repair_json from configs import dify_config from core.file import file_manager @@ -177,7 +179,8 @@ class Executor: raise RequestBodyError("json body type should have exactly one item") json_string = self.variable_pool.convert_template(data[0].value).text try: - json_object = json.loads(json_string, strict=False) + repaired = repair_json(json_string) + json_object = json.loads(repaired, strict=False) except json.JSONDecodeError as e: raise RequestBodyError(f"Failed to parse JSON: {json_string}") from e self.json = json_object @@ -332,7 +335,7 @@ class Executor: try: response = getattr(ssrf_proxy, self.method.lower())(**request_args) except (ssrf_proxy.MaxRetriesExceededError, httpx.RequestError) as e: - raise HttpRequestNodeError(str(e)) + raise HttpRequestNodeError(str(e)) from e # FIXME: fix type ignore, this maybe httpx type issue return response # type: ignore @@ -434,4 +437,4 @@ def _generate_random_string(n: int) -> str: >>> _generate_random_string(5) 'abcde' """ - return "".join([chr(randint(97, 122)) for _ in range(n)]) + return "".join(secrets.choice(string.ascii_lowercase) for _ in range(n)) diff --git a/api/core/workflow/nodes/http_request/node.py b/api/core/workflow/nodes/http_request/node.py index 6b1ac57c06..971e0f73e7 100644 --- a/api/core/workflow/nodes/http_request/node.py +++ b/api/core/workflow/nodes/http_request/node.py @@ -6,6 +6,7 @@ from typing import Any, Optional from configs import dify_config from core.file import File, FileTransferMethod from core.tools.tool_file_manager import ToolFileManager +from core.variables.segments import ArrayFileSegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.variable_entities import VariableSelector from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus @@ -60,6 +61,10 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): }, } + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: process_data = {} try: @@ -92,7 +97,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): status=WorkflowNodeExecutionStatus.SUCCEEDED, outputs={ "status_code": response.status_code, - "body": response.text if not files else "", + "body": response.text if not files.value else "", "headers": response.headers, "files": files, }, @@ -166,7 +171,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): return mapping - def extract_files(self, url: str, response: Response) -> list[File]: + def extract_files(self, url: str, response: Response) -> ArrayFileSegment: """ Extract files from response by checking both Content-Type header and URL """ @@ -178,7 +183,7 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): content_disposition_type = None if not is_file: - return files + return ArrayFileSegment(value=[]) if parsed_content_disposition: content_disposition_filename = parsed_content_disposition.get_filename() @@ -211,4 +216,4 @@ class HttpRequestNode(BaseNode[HttpRequestNodeData]): ) files.append(file) - return files + return ArrayFileSegment(value=files) diff --git a/api/core/workflow/nodes/if_else/if_else_node.py b/api/core/workflow/nodes/if_else/if_else_node.py index 976922f75d..22b748030c 100644 --- a/api/core/workflow/nodes/if_else/if_else_node.py +++ b/api/core/workflow/nodes/if_else/if_else_node.py @@ -1,4 +1,5 @@ -from typing import Literal +from collections.abc import Mapping, Sequence +from typing import Any, Literal from typing_extensions import deprecated @@ -16,6 +17,10 @@ class IfElseNode(BaseNode[IfElseNodeData]): _node_data_cls = IfElseNodeData _node_type = NodeType.IF_ELSE + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: """ Run node @@ -87,6 +92,22 @@ class IfElseNode(BaseNode[IfElseNodeData]): return data + @classmethod + def _extract_variable_selector_to_variable_mapping( + cls, + *, + graph_config: Mapping[str, Any], + node_id: str, + node_data: IfElseNodeData, + ) -> Mapping[str, Sequence[str]]: + var_mapping: dict[str, list[str]] = {} + for case in node_data.cases or []: + for condition in case.conditions: + key = "{}.#{}#".format(node_id, ".".join(condition.variable_selector)) + var_mapping[key] = condition.variable_selector + + return var_mapping + @deprecated("This function is deprecated. You should use the new cases structure.") def _should_not_use_old_function( diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index 2592823540..8b566c83cd 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -1,5 +1,6 @@ import contextvars import logging +import time import uuid from collections.abc import Generator, Mapping, Sequence from concurrent.futures import Future, wait @@ -7,10 +8,11 @@ from datetime import UTC, datetime from queue import Empty, Queue from typing import TYPE_CHECKING, Any, Optional, cast -from flask import Flask, current_app, has_request_context +from flask import Flask, current_app from configs import dify_config from core.variables import ArrayVariable, IntegerVariable, NoneVariable +from core.variables.segments import ArrayAnySegment, ArraySegment from core.workflow.entities.node_entities import ( NodeRunResult, ) @@ -37,6 +39,8 @@ from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType from core.workflow.nodes.event import NodeEvent, RunCompletedEvent from core.workflow.nodes.iteration.entities import ErrorHandleMode, IterationNodeData +from factories.variable_factory import build_segment +from libs.flask_utils import preserve_flask_contexts from .exc import ( InvalidIteratorValueError, @@ -71,6 +75,10 @@ class IterationNode(BaseNode[IterationNodeData]): }, } + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: """ Run the node. @@ -84,10 +92,17 @@ class IterationNode(BaseNode[IterationNodeData]): raise InvalidIteratorValueError(f"invalid iterator value: {variable}, please provide a list.") if isinstance(variable, NoneVariable) or len(variable.value) == 0: + # Try our best to preserve the type informat. + if isinstance(variable, ArraySegment): + output = variable.model_copy(update={"value": []}) + else: + output = ArrayAnySegment(value=[]) yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, - outputs={"output": []}, + # TODO(QuantumGhost): is it possible to compute the type of `output` + # from graph definition? + outputs={"output": output}, ) ) return @@ -119,8 +134,11 @@ class IterationNode(BaseNode[IterationNodeData]): variable_pool.add([self.node_id, "item"], iterator_list_value[0]) # init graph engine + from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.graph_engine import GraphEngine, GraphEngineThreadPool + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) + graph_engine = GraphEngine( tenant_id=self.tenant_id, app_id=self.app_id, @@ -132,7 +150,7 @@ class IterationNode(BaseNode[IterationNodeData]): call_depth=self.workflow_call_depth, graph=iteration_graph, graph_config=graph_config, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS, max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME, thread_pool_id=self.thread_pool_id, @@ -230,6 +248,7 @@ class IterationNode(BaseNode[IterationNodeData]): # Flatten the list of lists if isinstance(outputs, list) and all(isinstance(output, list) for output in outputs): outputs = [item for sublist in outputs for item in sublist] + output_segment = build_segment(outputs) yield IterationRunSucceededEvent( iteration_id=self.id, @@ -246,7 +265,7 @@ class IterationNode(BaseNode[IterationNodeData]): yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, - outputs={"output": outputs}, + outputs={"output": output_segment}, metadata={ WorkflowNodeExecutionMetadataKey.ITERATION_DURATION_MAP: iter_run_map, WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: graph_engine.graph_runtime_state.total_tokens, @@ -502,18 +521,52 @@ class IterationNode(BaseNode[IterationNodeData]): ) return elif self.node_data.error_handle_mode == ErrorHandleMode.TERMINATED: - yield IterationRunFailedEvent( - iteration_id=self.id, - iteration_node_id=self.node_id, - iteration_node_type=self.node_type, - iteration_node_data=self.node_data, - start_at=start_at, - inputs=inputs, - outputs={"output": None}, - steps=len(iterator_list_value), - metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, - error=event.error, + yield NodeInIterationFailedEvent( + **metadata_event.model_dump(), + ) + outputs[current_index] = None + + # clean nodes resources + for node_id in iteration_graph.node_ids: + variable_pool.remove([node_id]) + + # iteration run failed + if self.node_data.is_parallel: + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + parallel_mode_run_id=parallel_mode_run_id, + start_at=start_at, + inputs=inputs, + outputs={"output": outputs}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=event.error, + ) + else: + yield IterationRunFailedEvent( + iteration_id=self.id, + iteration_node_id=self.node_id, + iteration_node_type=self.node_type, + iteration_node_data=self.node_data, + start_at=start_at, + inputs=inputs, + outputs={"output": outputs}, + steps=len(iterator_list_value), + metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, + error=event.error, + ) + + # stop the iterator + yield RunCompletedEvent( + run_result=NodeRunResult( + status=WorkflowNodeExecutionStatus.FAILED, + error=event.error, + ) ) + return yield metadata_event current_output_segment = variable_pool.get(self.node_data.output_selector) @@ -583,23 +636,8 @@ class IterationNode(BaseNode[IterationNodeData]): """ run single iteration in parallel mode """ - for var, val in context.items(): - var.set(val) - - # FIXME(-LAN-): Save current user before entering new app context - from flask import g - - saved_user = None - if has_request_context() and hasattr(g, "_login_user"): - saved_user = g._login_user - - with flask_app.app_context(): - # Restore user in new app context - if saved_user is not None: - from flask import g - - g._login_user = saved_user + with preserve_flask_contexts(flask_app, context_vars=context): parallel_mode_run_id = uuid.uuid4().hex graph_engine_copy = graph_engine.create_copy() variable_pool_copy = graph_engine_copy.graph_runtime_state.variable_pool diff --git a/api/core/workflow/nodes/iteration/iteration_start_node.py b/api/core/workflow/nodes/iteration/iteration_start_node.py index bee481ebdb..9900aa225d 100644 --- a/api/core/workflow/nodes/iteration/iteration_start_node.py +++ b/api/core/workflow/nodes/iteration/iteration_start_node.py @@ -13,6 +13,10 @@ class IterationStartNode(BaseNode[IterationStartNodeData]): _node_data_cls = IterationStartNodeData _node_type = NodeType.ITERATION_START + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: """ Run the node. diff --git a/api/core/workflow/nodes/knowledge_retrieval/entities.py b/api/core/workflow/nodes/knowledge_retrieval/entities.py index d2e5a15545..19bdee4fe2 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/entities.py +++ b/api/core/workflow/nodes/knowledge_retrieval/entities.py @@ -132,3 +132,12 @@ class KnowledgeRetrievalNodeData(BaseNodeData): metadata_model_config: Optional[ModelConfig] = None metadata_filtering_conditions: Optional[MetadataFilteringCondition] = None vision: VisionConfig = Field(default_factory=VisionConfig) + + @property + def structured_output_enabled(self) -> bool: + # NOTE(QuantumGhost): Temporary workaround for issue #20725 + # (https://github.com/langgenius/dify/issues/20725). + # + # The proper fix would be to make `KnowledgeRetrievalNode` inherit + # from `BaseNode` instead of `LLMNode`. + return False diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index 5cf5848d54..f05d93d83e 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -24,6 +24,7 @@ from core.rag.entities.metadata_entities import Condition, MetadataCondition from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.variables import StringSegment +from core.variables.segments import ArrayObjectSegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.enums import NodeType @@ -70,6 +71,10 @@ class KnowledgeRetrievalNode(LLMNode): _node_data_cls = KnowledgeRetrievalNodeData # type: ignore _node_type = NodeType.KNOWLEDGE_RETRIEVAL + @classmethod + def version(cls): + return "1" + def _run(self) -> NodeRunResult: # type: ignore node_data = cast(KnowledgeRetrievalNodeData, self.node_data) # extract variables @@ -115,9 +120,12 @@ class KnowledgeRetrievalNode(LLMNode): # retrieve knowledge try: results = self._fetch_dataset_retriever(node_data=node_data, query=query) - outputs = {"result": results} + outputs = {"result": ArrayObjectSegment(value=results)} return NodeRunResult( - status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=variables, process_data=None, outputs=outputs + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs=variables, + process_data=None, + outputs=outputs, # type: ignore ) except KnowledgeRetrievalNodeError as e: @@ -136,6 +144,8 @@ class KnowledgeRetrievalNode(LLMNode): error=str(e), error_type=type(e).__name__, ) + finally: + db.session.close() def _fetch_dataset_retriever(self, node_data: KnowledgeRetrievalNodeData, query: str) -> list[dict[str, Any]]: available_datasets = [] @@ -163,6 +173,9 @@ class KnowledgeRetrievalNode(LLMNode): .all() ) + # avoid blocking at retrieval + db.session.close() + for dataset in results: # pass if dataset is not available if not dataset: @@ -482,6 +495,9 @@ class KnowledgeRetrievalNode(LLMNode): def _process_metadata_filter_func( self, sequence: int, condition: str, metadata_name: str, value: Optional[Any], filters: list ): + if value is None: + return + key = f"{metadata_name}_{sequence}" key_value = f"{metadata_name}_{sequence}_value" match condition: diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index e698d3f5d8..3c9ba44cf1 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -3,6 +3,7 @@ from typing import Any, Literal, Union from core.file import File from core.variables import ArrayFileSegment, ArrayNumberSegment, ArrayStringSegment +from core.variables.segments import ArrayAnySegment, ArraySegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.base import BaseNode @@ -16,6 +17,10 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): _node_data_cls = ListOperatorNodeData _node_type = NodeType.LIST_OPERATOR + @classmethod + def version(cls) -> str: + return "1" + def _run(self): inputs: dict[str, list] = {} process_data: dict[str, list] = {} @@ -30,7 +35,11 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): if not variable.value: inputs = {"variable": []} process_data = {"variable": []} - outputs = {"result": [], "first_record": None, "last_record": None} + if isinstance(variable, ArraySegment): + result = variable.model_copy(update={"value": []}) + else: + result = ArrayAnySegment(value=[]) + outputs = {"result": result, "first_record": None, "last_record": None} return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=inputs, @@ -71,7 +80,7 @@ class ListOperatorNode(BaseNode[ListOperatorNodeData]): variable = self._apply_slice(variable) outputs = { - "result": variable.value, + "result": variable, "first_record": variable.value[0] if variable.value else None, "last_record": variable.value[-1] if variable.value else None, } diff --git a/api/core/workflow/nodes/llm/file_saver.py b/api/core/workflow/nodes/llm/file_saver.py index c85baade03..a4b45ce652 100644 --- a/api/core/workflow/nodes/llm/file_saver.py +++ b/api/core/workflow/nodes/llm/file_saver.py @@ -119,9 +119,6 @@ class FileSaverImpl(LLMFileSaver): size=len(data), related_id=tool_file.id, url=url, - # TODO(QuantumGhost): how should I set the following key? - # What's the difference between `remote_url` and `url`? - # What's the purpose of `storage_key` and `dify_model_identity`? storage_key=tool_file.file_key, ) diff --git a/api/core/workflow/nodes/llm/llm_utils.py b/api/core/workflow/nodes/llm/llm_utils.py new file mode 100644 index 0000000000..0966c87a1d --- /dev/null +++ b/api/core/workflow/nodes/llm/llm_utils.py @@ -0,0 +1,156 @@ +from collections.abc import Sequence +from datetime import UTC, datetime +from typing import Optional, cast + +from sqlalchemy import select, update +from sqlalchemy.orm import Session + +from configs import dify_config +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.entities.provider_entities import QuotaUnit +from core.file.models import File +from core.memory.token_buffer_memory import TokenBufferMemory +from core.model_manager import ModelInstance, ModelManager +from core.model_runtime.entities.llm_entities import LLMUsage +from core.model_runtime.entities.model_entities import ModelType +from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from core.plugin.entities.plugin import ModelProviderID +from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.variables.segments import ArrayAnySegment, ArrayFileSegment, FileSegment, NoneSegment, StringSegment +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.enums import SystemVariableKey +from core.workflow.nodes.llm.entities import ModelConfig +from models import db +from models.model import Conversation +from models.provider import Provider, ProviderType + +from .exc import InvalidVariableTypeError, LLMModeRequiredError, ModelNotExistError + + +def fetch_model_config( + tenant_id: str, node_data_model: ModelConfig +) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: + if not node_data_model.mode: + raise LLMModeRequiredError("LLM mode is required.") + + model = ModelManager().get_model_instance( + tenant_id=tenant_id, + model_type=ModelType.LLM, + provider=node_data_model.provider, + model=node_data_model.name, + ) + + model.model_type_instance = cast(LargeLanguageModel, model.model_type_instance) + + # check model + provider_model = model.provider_model_bundle.configuration.get_provider_model( + model=node_data_model.name, model_type=ModelType.LLM + ) + + if provider_model is None: + raise ModelNotExistError(f"Model {node_data_model.name} not exist.") + provider_model.raise_for_status() + + # model config + stop: list[str] = [] + if "stop" in node_data_model.completion_params: + stop = node_data_model.completion_params.pop("stop") + + model_schema = model.model_type_instance.get_model_schema(node_data_model.name, model.credentials) + if not model_schema: + raise ModelNotExistError(f"Model {node_data_model.name} not exist.") + + return model, ModelConfigWithCredentialsEntity( + provider=node_data_model.provider, + model=node_data_model.name, + model_schema=model_schema, + mode=node_data_model.mode, + provider_model_bundle=model.provider_model_bundle, + credentials=model.credentials, + parameters=node_data_model.completion_params, + stop=stop, + ) + + +def fetch_files(variable_pool: VariablePool, selector: Sequence[str]) -> Sequence["File"]: + variable = variable_pool.get(selector) + if variable is None: + return [] + elif isinstance(variable, FileSegment): + return [variable.value] + elif isinstance(variable, ArrayFileSegment): + return variable.value + elif isinstance(variable, NoneSegment | ArrayAnySegment): + return [] + raise InvalidVariableTypeError(f"Invalid variable type: {type(variable)}") + + +def fetch_memory( + variable_pool: VariablePool, app_id: str, node_data_memory: Optional[MemoryConfig], model_instance: ModelInstance +) -> Optional[TokenBufferMemory]: + if not node_data_memory: + return None + + # get conversation id + conversation_id_variable = variable_pool.get(["sys", SystemVariableKey.CONVERSATION_ID.value]) + if not isinstance(conversation_id_variable, StringSegment): + return None + conversation_id = conversation_id_variable.value + + with Session(db.engine, expire_on_commit=False) as session: + stmt = select(Conversation).where(Conversation.app_id == app_id, Conversation.id == conversation_id) + conversation = session.scalar(stmt) + if not conversation: + return None + + memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance) + return memory + + +def deduct_llm_quota(tenant_id: str, model_instance: ModelInstance, usage: LLMUsage) -> None: + provider_model_bundle = model_instance.provider_model_bundle + provider_configuration = provider_model_bundle.configuration + + if provider_configuration.using_provider_type != ProviderType.SYSTEM: + return + + system_configuration = provider_configuration.system_configuration + + quota_unit = None + for quota_configuration in system_configuration.quota_configurations: + if quota_configuration.quota_type == system_configuration.current_quota_type: + quota_unit = quota_configuration.quota_unit + + if quota_configuration.quota_limit == -1: + return + + break + + used_quota = None + if quota_unit: + if quota_unit == QuotaUnit.TOKENS: + used_quota = usage.total_tokens + elif quota_unit == QuotaUnit.CREDITS: + used_quota = dify_config.get_model_credits(model_instance.model) + else: + used_quota = 1 + + if used_quota is not None and system_configuration.current_quota_type is not None: + with Session(db.engine) as session: + stmt = ( + update(Provider) + .where( + Provider.tenant_id == tenant_id, + # TODO: Use provider name with prefix after the data migration. + Provider.provider_name == ModelProviderID(model_instance.provider).provider_name, + Provider.provider_type == ProviderType.SYSTEM.value, + Provider.quota_type == system_configuration.current_quota_type.value, + Provider.quota_limit > Provider.quota_used, + ) + .values( + quota_used=Provider.quota_used + used_quota, + last_used=datetime.now(tz=UTC).replace(tzinfo=None), + ) + ) + session.execute(stmt) + session.commit() diff --git a/api/core/workflow/nodes/llm/node.py b/api/core/workflow/nodes/llm/node.py index ee181cf3bf..9bfb402dc8 100644 --- a/api/core/workflow/nodes/llm/node.py +++ b/api/core/workflow/nodes/llm/node.py @@ -3,18 +3,13 @@ import io import json import logging from collections.abc import Generator, Mapping, Sequence -from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Optional, cast -import json_repair -from sqlalchemy import select, update -from sqlalchemy.orm import Session - -from configs import dify_config from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.entities.provider_entities import QuotaUnit from core.file import FileType, file_manager from core.helper.code_executor import CodeExecutor, CodeLanguage +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance, ModelManager from core.model_runtime.entities import ( @@ -23,7 +18,13 @@ from core.model_runtime.entities import ( PromptMessageContentType, TextPromptMessageContent, ) -from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMUsage +from core.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkWithStructuredOutput, + LLMStructuredOutput, + LLMUsage, +) from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, PromptMessageContentUnionTypes, @@ -36,16 +37,13 @@ from core.model_runtime.entities.model_entities import ( ModelFeature, ModelPropertyKey, ModelType, - ParameterRule, ) from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.utils.encoders import jsonable_encoder -from core.plugin.entities.plugin import ModelProviderID from core.prompt.entities.advanced_prompt_entities import CompletionModelPromptTemplate, MemoryConfig from core.prompt.utils.prompt_message_util import PromptMessageUtil from core.rag.entities.citation_metadata import RetrievalSourceMetadata from core.variables import ( - ArrayAnySegment, ArrayFileSegment, ArraySegment, FileSegment, @@ -69,16 +67,9 @@ from core.workflow.nodes.event import ( RunRetrieverResourceEvent, RunStreamChunkEvent, ) -from core.workflow.utils.structured_output.entities import ( - ResponseFormat, - SpecialModelType, -) -from core.workflow.utils.structured_output.prompt import STRUCTURED_OUTPUT_PROMPT from core.workflow.utils.variable_template_parser import VariableTemplateParser -from extensions.ext_database import db -from models.model import Conversation -from models.provider import Provider, ProviderType +from . import llm_utils from .entities import ( LLMNodeChatModelMessage, LLMNodeCompletionModelPromptTemplate, @@ -88,7 +79,6 @@ from .entities import ( from .exc import ( InvalidContextStructureError, InvalidVariableTypeError, - LLMModeRequiredError, LLMNodeError, MemoryRolePrefixRequiredError, ModelNotExistError, @@ -148,18 +138,17 @@ class LLMNode(BaseNode[LLMNodeData]): ) self._llm_file_saver = llm_file_saver - def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: - def process_structured_output(text: str) -> Optional[dict[str, Any]]: - """Process structured output if enabled""" - if not self.node_data.structured_output_enabled or not self.node_data.structured_output: - return None - return self._parse_structured_output(text) + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: node_inputs: Optional[dict[str, Any]] = None process_data = None result_text = "" usage = LLMUsage.empty_usage() finish_reason = None + variable_pool = self.graph_runtime_state.variable_pool try: # init messages template @@ -178,7 +167,10 @@ class LLMNode(BaseNode[LLMNodeData]): # fetch files files = ( - self._fetch_files(selector=self.node_data.vision.configs.variable_selector) + llm_utils.fetch_files( + variable_pool=variable_pool, + selector=self.node_data.vision.configs.variable_selector, + ) if self.node_data.vision.enabled else [] ) @@ -200,15 +192,18 @@ class LLMNode(BaseNode[LLMNodeData]): model_instance, model_config = self._fetch_model_config(self.node_data.model) # fetch memory - memory = self._fetch_memory(node_data_memory=self.node_data.memory, model_instance=model_instance) + memory = llm_utils.fetch_memory( + variable_pool=variable_pool, + app_id=self.app_id, + node_data_memory=self.node_data.memory, + model_instance=model_instance, + ) query = None if self.node_data.memory: query = self.node_data.memory.query_prompt_template if not query and ( - query_variable := self.graph_runtime_state.variable_pool.get( - (SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY) - ) + query_variable := variable_pool.get((SYSTEM_VARIABLE_NODE_ID, SystemVariableKey.QUERY)) ): query = query_variable.text @@ -222,19 +217,10 @@ class LLMNode(BaseNode[LLMNodeData]): memory_config=self.node_data.memory, vision_enabled=self.node_data.vision.enabled, vision_detail=self.node_data.vision.configs.detail, - variable_pool=self.graph_runtime_state.variable_pool, + variable_pool=variable_pool, jinja2_variables=self.node_data.prompt_config.jinja2_variables, ) - process_data = { - "model_mode": model_config.mode, - "prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving( - model_mode=model_config.mode, prompt_messages=prompt_messages - ), - "model_provider": model_config.provider, - "model_name": model_config.model, - } - # handle invoke result generator = self._invoke_llm( node_data_model=self.node_data.model, @@ -243,6 +229,8 @@ class LLMNode(BaseNode[LLMNodeData]): stop=stop, ) + structured_output: LLMStructuredOutput | None = None + for event in generator: if isinstance(event, RunStreamChunkEvent): yield event @@ -251,14 +239,27 @@ class LLMNode(BaseNode[LLMNodeData]): usage = event.usage finish_reason = event.finish_reason # deduct quota - self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage) + llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage) break + elif isinstance(event, LLMStructuredOutput): + structured_output = event + + process_data = { + "model_mode": model_config.mode, + "prompts": PromptMessageUtil.prompt_messages_to_prompt_for_saving( + model_mode=model_config.mode, prompt_messages=prompt_messages + ), + "usage": jsonable_encoder(usage), + "finish_reason": finish_reason, + "model_provider": model_config.provider, + "model_name": model_config.model, + } + outputs = {"text": result_text, "usage": jsonable_encoder(usage), "finish_reason": finish_reason} - structured_output = process_structured_output(result_text) if structured_output: - outputs["structured_output"] = structured_output + outputs["structured_output"] = structured_output.structured_output if self._file_outputs is not None: - outputs["files"] = self._file_outputs + outputs["files"] = ArrayFileSegment(value=self._file_outputs) yield RunCompletedEvent( run_result=NodeRunResult( @@ -301,20 +302,40 @@ class LLMNode(BaseNode[LLMNodeData]): model_instance: ModelInstance, prompt_messages: Sequence[PromptMessage], stop: Optional[Sequence[str]] = None, - ) -> Generator[NodeEvent, None, None]: - invoke_result = model_instance.invoke_llm( - prompt_messages=list(prompt_messages), - model_parameters=node_data_model.completion_params, - stop=list(stop or []), - stream=True, - user=self.user_id, + ) -> Generator[NodeEvent | LLMStructuredOutput, None, None]: + model_schema = model_instance.model_type_instance.get_model_schema( + node_data_model.name, model_instance.credentials ) + if not model_schema: + raise ValueError(f"Model schema not found for {node_data_model.name}") + + if self.node_data.structured_output_enabled: + output_schema = self._fetch_structured_output_schema() + invoke_result = invoke_llm_with_structured_output( + provider=model_instance.provider, + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=output_schema, + model_parameters=node_data_model.completion_params, + stop=list(stop or []), + stream=True, + user=self.user_id, + ) + else: + invoke_result = model_instance.invoke_llm( + prompt_messages=list(prompt_messages), + model_parameters=node_data_model.completion_params, + stop=list(stop or []), + stream=True, + user=self.user_id, + ) return self._handle_invoke_result(invoke_result=invoke_result) def _handle_invoke_result( - self, invoke_result: LLMResult | Generator[LLMResultChunk, None, None] - ) -> Generator[NodeEvent, None, None]: + self, invoke_result: LLMResult | Generator[LLMResultChunk | LLMStructuredOutput, None, None] + ) -> Generator[NodeEvent | LLMStructuredOutput, None, None]: # For blocking mode if isinstance(invoke_result, LLMResult): event = self._handle_blocking_result(invoke_result=invoke_result) @@ -328,23 +349,32 @@ class LLMNode(BaseNode[LLMNodeData]): usage = LLMUsage.empty_usage() finish_reason = None full_text_buffer = io.StringIO() - for result in invoke_result: - contents = result.delta.message.content - for text_part in self._save_multimodal_output_and_convert_result_to_markdown(contents): - full_text_buffer.write(text_part) - yield RunStreamChunkEvent(chunk_content=text_part, from_variable_selector=[self.node_id, "text"]) - - # Update the whole metadata - if not model and result.model: - model = result.model - if len(prompt_messages) == 0: - # TODO(QuantumGhost): it seems that this update has no visable effect. - # What's the purpose of the line below? - prompt_messages = list(result.prompt_messages) - if usage.prompt_tokens == 0 and result.delta.usage: - usage = result.delta.usage - if finish_reason is None and result.delta.finish_reason: - finish_reason = result.delta.finish_reason + # Consume the invoke result and handle generator exception + try: + for result in invoke_result: + if isinstance(result, LLMResultChunkWithStructuredOutput): + yield result + if isinstance(result, LLMResultChunk): + contents = result.delta.message.content + for text_part in self._save_multimodal_output_and_convert_result_to_markdown(contents): + full_text_buffer.write(text_part) + yield RunStreamChunkEvent( + chunk_content=text_part, from_variable_selector=[self.node_id, "text"] + ) + + # Update the whole metadata + if not model and result.model: + model = result.model + if len(prompt_messages) == 0: + # TODO(QuantumGhost): it seems that this update has no visable effect. + # What's the purpose of the line below? + prompt_messages = list(result.prompt_messages) + if usage.prompt_tokens == 0 and result.delta.usage: + usage = result.delta.usage + if finish_reason is None and result.delta.finish_reason: + finish_reason = result.delta.finish_reason + except OutputParserError as e: + raise LLMNodeError(f"Failed to parse structured output: {e}") yield ModelInvokeCompletedEvent(text=full_text_buffer.getvalue(), usage=usage, finish_reason=finish_reason) @@ -447,18 +477,6 @@ class LLMNode(BaseNode[LLMNodeData]): return inputs - def _fetch_files(self, *, selector: Sequence[str]) -> Sequence["File"]: - variable = self.graph_runtime_state.variable_pool.get(selector) - if variable is None: - return [] - elif isinstance(variable, FileSegment): - return [variable.value] - elif isinstance(variable, ArrayFileSegment): - return variable.value - elif isinstance(variable, NoneSegment | ArrayAnySegment): - return [] - raise InvalidVariableTypeError(f"Invalid variable type: {type(variable)}") - def _fetch_context(self, node_data: LLMNodeData): if not node_data.context.enabled: return @@ -524,79 +542,19 @@ class LLMNode(BaseNode[LLMNodeData]): def _fetch_model_config( self, node_data_model: ModelConfig ) -> tuple[ModelInstance, ModelConfigWithCredentialsEntity]: - if not node_data_model.mode: - raise LLMModeRequiredError("LLM mode is required.") - - model = ModelManager().get_model_instance( - tenant_id=self.tenant_id, - model_type=ModelType.LLM, - provider=node_data_model.provider, - model=node_data_model.name, + model, model_config_with_cred = llm_utils.fetch_model_config( + tenant_id=self.tenant_id, node_data_model=node_data_model ) - - model.model_type_instance = cast(LargeLanguageModel, model.model_type_instance) - - # check model - provider_model = model.provider_model_bundle.configuration.get_provider_model( - model=node_data_model.name, model_type=ModelType.LLM - ) - - if provider_model is None: - raise ModelNotExistError(f"Model {node_data_model.name} not exist.") - provider_model.raise_for_status() - - # model config - stop: list[str] = [] - if "stop" in node_data_model.completion_params: - stop = node_data_model.completion_params.pop("stop") + completion_params = model_config_with_cred.parameters model_schema = model.model_type_instance.get_model_schema(node_data_model.name, model.credentials) if not model_schema: raise ModelNotExistError(f"Model {node_data_model.name} not exist.") - if self.node_data.structured_output_enabled: - if model_schema.support_structure_output: - node_data_model.completion_params = self._handle_native_json_schema( - node_data_model.completion_params, model_schema.parameter_rules - ) - else: - # Set appropriate response format based on model capabilities - self._set_response_format(node_data_model.completion_params, model_schema.parameter_rules) - - return model, ModelConfigWithCredentialsEntity( - provider=node_data_model.provider, - model=node_data_model.name, - model_schema=model_schema, - mode=node_data_model.mode, - provider_model_bundle=model.provider_model_bundle, - credentials=model.credentials, - parameters=node_data_model.completion_params, - stop=stop, - ) - - def _fetch_memory( - self, node_data_memory: Optional[MemoryConfig], model_instance: ModelInstance - ) -> Optional[TokenBufferMemory]: - if not node_data_memory: - return None - - # get conversation id - conversation_id_variable = self.graph_runtime_state.variable_pool.get( - ["sys", SystemVariableKey.CONVERSATION_ID.value] - ) - if not isinstance(conversation_id_variable, StringSegment): - return None - conversation_id = conversation_id_variable.value - - with Session(db.engine, expire_on_commit=False) as session: - stmt = select(Conversation).where(Conversation.app_id == self.app_id, Conversation.id == conversation_id) - conversation = session.scalar(stmt) - if not conversation: - return None - - memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance) - - return memory + model_config_with_cred.parameters = completion_params + # NOTE(-LAN-): This line modify the `self.node_data.model`, which is used in `_invoke_llm()`. + node_data_model.completion_params = completion_params + return model, model_config_with_cred def _fetch_prompt_messages( self, @@ -775,90 +733,17 @@ class LLMNode(BaseNode[LLMNodeData]): model = ModelManager().get_model_instance( tenant_id=self.tenant_id, model_type=ModelType.LLM, - provider=self.node_data.model.provider, - model=self.node_data.model.name, + provider=model_config.provider, + model=model_config.model, ) model_schema = model.model_type_instance.get_model_schema( - model=self.node_data.model.name, + model=model_config.model, credentials=model.credentials, ) if not model_schema: - raise ModelNotExistError(f"Model {self.node_data.model.name} not exist.") - if self.node_data.structured_output_enabled: - if not model_schema.support_structure_output: - filtered_prompt_messages = self._handle_prompt_based_schema( - prompt_messages=filtered_prompt_messages, - ) + raise ModelNotExistError(f"Model {model_config.model} not exist.") return filtered_prompt_messages, model_config.stop - def _parse_structured_output(self, result_text: str) -> dict[str, Any]: - structured_output: dict[str, Any] = {} - try: - parsed = json.loads(result_text) - if not isinstance(parsed, dict): - raise LLMNodeError(f"Failed to parse structured output: {result_text}") - structured_output = parsed - except json.JSONDecodeError as e: - # if the result_text is not a valid json, try to repair it - parsed = json_repair.loads(result_text) - if not isinstance(parsed, dict): - # handle reasoning model like deepseek-r1 got '\n\n\n' prefix - if isinstance(parsed, list): - parsed = next((item for item in parsed if isinstance(item, dict)), {}) - else: - raise LLMNodeError(f"Failed to parse structured output: {result_text}") - structured_output = parsed - return structured_output - - @classmethod - def deduct_llm_quota(cls, tenant_id: str, model_instance: ModelInstance, usage: LLMUsage) -> None: - provider_model_bundle = model_instance.provider_model_bundle - provider_configuration = provider_model_bundle.configuration - - if provider_configuration.using_provider_type != ProviderType.SYSTEM: - return - - system_configuration = provider_configuration.system_configuration - - quota_unit = None - for quota_configuration in system_configuration.quota_configurations: - if quota_configuration.quota_type == system_configuration.current_quota_type: - quota_unit = quota_configuration.quota_unit - - if quota_configuration.quota_limit == -1: - return - - break - - used_quota = None - if quota_unit: - if quota_unit == QuotaUnit.TOKENS: - used_quota = usage.total_tokens - elif quota_unit == QuotaUnit.CREDITS: - used_quota = dify_config.get_model_credits(model_instance.model) - else: - used_quota = 1 - - if used_quota is not None and system_configuration.current_quota_type is not None: - with Session(db.engine) as session: - stmt = ( - update(Provider) - .where( - Provider.tenant_id == tenant_id, - # TODO: Use provider name with prefix after the data migration. - Provider.provider_name == ModelProviderID(model_instance.provider).provider_name, - Provider.provider_type == ProviderType.SYSTEM.value, - Provider.quota_type == system_configuration.current_quota_type.value, - Provider.quota_limit > Provider.quota_used, - ) - .values( - quota_used=Provider.quota_used + used_quota, - last_used=datetime.now(tz=UTC).replace(tzinfo=None), - ) - ) - session.execute(stmt) - session.commit() - @classmethod def _extract_variable_selector_to_variable_mapping( cls, @@ -1048,104 +933,6 @@ class LLMNode(BaseNode[LLMNodeData]): self._file_outputs.append(saved_file) return saved_file - def _handle_native_json_schema(self, model_parameters: dict, rules: list[ParameterRule]) -> dict: - """ - Handle structured output for models with native JSON schema support. - - :param model_parameters: Model parameters to update - :param rules: Model parameter rules - :return: Updated model parameters with JSON schema configuration - """ - # Process schema according to model requirements - schema = self._fetch_structured_output_schema() - schema_json = self._prepare_schema_for_model(schema) - - # Set JSON schema in parameters - model_parameters["json_schema"] = json.dumps(schema_json, ensure_ascii=False) - - # Set appropriate response format if required by the model - for rule in rules: - if rule.name == "response_format" and ResponseFormat.JSON_SCHEMA.value in rule.options: - model_parameters["response_format"] = ResponseFormat.JSON_SCHEMA.value - - return model_parameters - - def _handle_prompt_based_schema(self, prompt_messages: Sequence[PromptMessage]) -> list[PromptMessage]: - """ - Handle structured output for models without native JSON schema support. - This function modifies the prompt messages to include schema-based output requirements. - - Args: - prompt_messages: Original sequence of prompt messages - - Returns: - list[PromptMessage]: Updated prompt messages with structured output requirements - """ - # Convert schema to string format - schema_str = json.dumps(self._fetch_structured_output_schema(), ensure_ascii=False) - - # Find existing system prompt with schema placeholder - system_prompt = next( - (prompt for prompt in prompt_messages if isinstance(prompt, SystemPromptMessage)), - None, - ) - structured_output_prompt = STRUCTURED_OUTPUT_PROMPT.replace("{{schema}}", schema_str) - # Prepare system prompt content - system_prompt_content = ( - structured_output_prompt + "\n\n" + system_prompt.content - if system_prompt and isinstance(system_prompt.content, str) - else structured_output_prompt - ) - system_prompt = SystemPromptMessage(content=system_prompt_content) - - # Extract content from the last user message - - filtered_prompts = [prompt for prompt in prompt_messages if not isinstance(prompt, SystemPromptMessage)] - updated_prompt = [system_prompt] + filtered_prompts - - return updated_prompt - - def _set_response_format(self, model_parameters: dict, rules: list) -> None: - """ - Set the appropriate response format parameter based on model rules. - - :param model_parameters: Model parameters to update - :param rules: Model parameter rules - """ - for rule in rules: - if rule.name == "response_format": - if ResponseFormat.JSON.value in rule.options: - model_parameters["response_format"] = ResponseFormat.JSON.value - elif ResponseFormat.JSON_OBJECT.value in rule.options: - model_parameters["response_format"] = ResponseFormat.JSON_OBJECT.value - - def _prepare_schema_for_model(self, schema: dict) -> dict: - """ - Prepare JSON schema based on model requirements. - - Different models have different requirements for JSON schema formatting. - This function handles these differences. - - :param schema: The original JSON schema - :return: Processed schema compatible with the current model - """ - - # Deep copy to avoid modifying the original schema - processed_schema = schema.copy() - - # Convert boolean types to string types (common requirement) - convert_boolean_to_string(processed_schema) - - # Apply model-specific transformations - if SpecialModelType.GEMINI in self.node_data.model.name: - remove_additional_properties(processed_schema) - return processed_schema - elif SpecialModelType.OLLAMA in self.node_data.model.provider: - return processed_schema - else: - # Default format with name field - return {"schema": processed_schema, "name": "llm_response"} - def _fetch_model_schema(self, provider: str) -> AIModelEntity | None: """ Fetch model schema @@ -1357,49 +1144,3 @@ def _handle_completion_template( ) prompt_messages.append(prompt_message) return prompt_messages - - -def remove_additional_properties(schema: dict) -> None: - """ - Remove additionalProperties fields from JSON schema. - Used for models like Gemini that don't support this property. - - :param schema: JSON schema to modify in-place - """ - if not isinstance(schema, dict): - return - - # Remove additionalProperties at current level - schema.pop("additionalProperties", None) - - # Process nested structures recursively - for value in schema.values(): - if isinstance(value, dict): - remove_additional_properties(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, dict): - remove_additional_properties(item) - - -def convert_boolean_to_string(schema: dict) -> None: - """ - Convert boolean type specifications to string in JSON schema. - - :param schema: JSON schema to modify in-place - """ - if not isinstance(schema, dict): - return - - # Check for boolean type at current level - if schema.get("type") == "boolean": - schema["type"] = "string" - - # Process nested dictionaries and lists recursively - for value in schema.values(): - if isinstance(value, dict): - convert_boolean_to_string(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, dict): - convert_boolean_to_string(item) diff --git a/api/core/workflow/nodes/loop/entities.py b/api/core/workflow/nodes/loop/entities.py index 3f4a5edab9..d04e0bfae1 100644 --- a/api/core/workflow/nodes/loop/entities.py +++ b/api/core/workflow/nodes/loop/entities.py @@ -1,11 +1,29 @@ from collections.abc import Mapping -from typing import Any, Literal, Optional +from typing import Annotated, Any, Literal, Optional -from pydantic import BaseModel, Field +from pydantic import AfterValidator, BaseModel, Field +from core.variables.types import SegmentType from core.workflow.nodes.base import BaseLoopNodeData, BaseLoopState, BaseNodeData from core.workflow.utils.condition.entities import Condition +_VALID_VAR_TYPE = frozenset( + [ + SegmentType.STRING, + SegmentType.NUMBER, + SegmentType.OBJECT, + SegmentType.ARRAY_STRING, + SegmentType.ARRAY_NUMBER, + SegmentType.ARRAY_OBJECT, + ] +) + + +def _is_valid_var_type(seg_type: SegmentType) -> SegmentType: + if seg_type not in _VALID_VAR_TYPE: + raise ValueError(...) + return seg_type + class LoopVariableData(BaseModel): """ @@ -13,7 +31,7 @@ class LoopVariableData(BaseModel): """ label: str - var_type: Literal["string", "number", "object", "array[string]", "array[number]", "array[object]"] + var_type: Annotated[SegmentType, AfterValidator(_is_valid_var_type)] value_type: Literal["variable", "constant"] value: Optional[Any | list[str]] = None diff --git a/api/core/workflow/nodes/loop/loop_end_node.py b/api/core/workflow/nodes/loop/loop_end_node.py index 327b9e234b..b144021bab 100644 --- a/api/core/workflow/nodes/loop/loop_end_node.py +++ b/api/core/workflow/nodes/loop/loop_end_node.py @@ -13,6 +13,10 @@ class LoopEndNode(BaseNode[LoopEndNodeData]): _node_data_cls = LoopEndNodeData _node_type = NodeType.LOOP_END + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: """ Run the node. diff --git a/api/core/workflow/nodes/loop/loop_node.py b/api/core/workflow/nodes/loop/loop_node.py index fafa205386..20501d0317 100644 --- a/api/core/workflow/nodes/loop/loop_node.py +++ b/api/core/workflow/nodes/loop/loop_node.py @@ -1,19 +1,15 @@ import json import logging +import time from collections.abc import Generator, Mapping, Sequence from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Literal, cast from configs import dify_config from core.variables import ( - ArrayNumberSegment, - ArrayObjectSegment, - ArrayStringSegment, IntegerSegment, - ObjectSegment, Segment, SegmentType, - StringSegment, ) from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus @@ -38,6 +34,7 @@ from core.workflow.nodes.enums import NodeType from core.workflow.nodes.event import NodeEvent, RunCompletedEvent from core.workflow.nodes.loop.entities import LoopNodeData from core.workflow.utils.condition.processor import ConditionProcessor +from factories.variable_factory import TypeMismatchError, build_segment_with_type if TYPE_CHECKING: from core.workflow.entities.variable_pool import VariablePool @@ -54,6 +51,10 @@ class LoopNode(BaseNode[LoopNodeData]): _node_data_cls = LoopNodeData _node_type = NodeType.LOOP + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]: """Run the node.""" # Get inputs @@ -97,8 +98,11 @@ class LoopNode(BaseNode[LoopNodeData]): loop_variable_selectors[loop_variable.label] = variable_selector inputs[loop_variable.label] = processed_segment.value + from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.graph_engine import GraphEngine + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) + graph_engine = GraphEngine( tenant_id=self.tenant_id, app_id=self.app_id, @@ -110,7 +114,7 @@ class LoopNode(BaseNode[LoopNodeData]): call_depth=self.workflow_call_depth, graph=loop_graph, graph_config=self.graph_config, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS, max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME, thread_pool_id=self.thread_pool_id, @@ -482,6 +486,13 @@ class LoopNode(BaseNode[LoopNodeData]): variable_mapping.update(sub_node_variable_mapping) + for loop_variable in node_data.loop_variables or []: + if loop_variable.value_type == "variable": + assert loop_variable.value is not None, "Loop variable value must be provided for variable type" + # add loop variable to variable mapping + selector = loop_variable.value + variable_mapping[f"{node_id}.{loop_variable.label}"] = selector + # remove variable out from loop variable_mapping = { key: value for key, value in variable_mapping.items() if value[0] not in loop_graph.node_ids @@ -490,23 +501,21 @@ class LoopNode(BaseNode[LoopNodeData]): return variable_mapping @staticmethod - def _get_segment_for_constant(var_type: str, value: Any) -> Segment: + def _get_segment_for_constant(var_type: SegmentType, value: Any) -> Segment: """Get the appropriate segment type for a constant value.""" - segment_mapping: dict[str, tuple[type[Segment], SegmentType]] = { - "string": (StringSegment, SegmentType.STRING), - "number": (IntegerSegment, SegmentType.NUMBER), - "object": (ObjectSegment, SegmentType.OBJECT), - "array[string]": (ArrayStringSegment, SegmentType.ARRAY_STRING), - "array[number]": (ArrayNumberSegment, SegmentType.ARRAY_NUMBER), - "array[object]": (ArrayObjectSegment, SegmentType.ARRAY_OBJECT), - } if var_type in ["array[string]", "array[number]", "array[object]"]: - if value: + if value and isinstance(value, str): value = json.loads(value) else: value = [] - segment_info = segment_mapping.get(var_type) - if not segment_info: - raise ValueError(f"Invalid variable type: {var_type}") - segment_class, value_type = segment_info - return segment_class(value=value, value_type=value_type) + try: + return build_segment_with_type(var_type, value) + except TypeMismatchError as type_exc: + # Attempt to parse the value as a JSON-encoded string, if applicable. + if not isinstance(value, str): + raise + try: + value = json.loads(value) + except ValueError: + raise type_exc + return build_segment_with_type(var_type, value) diff --git a/api/core/workflow/nodes/loop/loop_start_node.py b/api/core/workflow/nodes/loop/loop_start_node.py index 5a15f36044..f5e38b7516 100644 --- a/api/core/workflow/nodes/loop/loop_start_node.py +++ b/api/core/workflow/nodes/loop/loop_start_node.py @@ -13,6 +13,10 @@ class LoopStartNode(BaseNode[LoopStartNodeData]): _node_data_cls = LoopStartNodeData _node_type = NodeType.LOOP_START + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: """ Run the node. diff --git a/api/core/workflow/nodes/node_mapping.py b/api/core/workflow/nodes/node_mapping.py index 1f1be59542..ccfaec4a8c 100644 --- a/api/core/workflow/nodes/node_mapping.py +++ b/api/core/workflow/nodes/node_mapping.py @@ -25,6 +25,11 @@ from core.workflow.nodes.variable_assigner.v2 import VariableAssignerNode as Var LATEST_VERSION = "latest" +# NOTE(QuantumGhost): This should be in sync with subclasses of BaseNode. +# Specifically, if you have introduced new node types, you should add them here. +# +# TODO(QuantumGhost): This could be automated with either metaclass or `__init_subclass__` +# hook. Try to avoid duplication of node information. NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { NodeType.START: { LATEST_VERSION: StartNode, @@ -68,6 +73,7 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { }, NodeType.TOOL: { LATEST_VERSION: ToolNode, + "2": ToolNode, "1": ToolNode, }, NodeType.VARIABLE_AGGREGATOR: { @@ -117,6 +123,7 @@ NODE_TYPE_CLASSES_MAPPING: Mapping[NodeType, Mapping[str, type[BaseNode]]] = { }, NodeType.AGENT: { LATEST_VERSION: AgentNode, + "2": AgentNode, "1": AgentNode, }, } diff --git a/api/core/workflow/nodes/parameter_extractor/entities.py b/api/core/workflow/nodes/parameter_extractor/entities.py index 369eb13b04..916778d167 100644 --- a/api/core/workflow/nodes/parameter_extractor/entities.py +++ b/api/core/workflow/nodes/parameter_extractor/entities.py @@ -7,6 +7,10 @@ from core.workflow.nodes.base import BaseNodeData from core.workflow.nodes.llm import ModelConfig, VisionConfig +class _ParameterConfigError(Exception): + pass + + class ParameterConfig(BaseModel): """ Parameter Config. @@ -27,6 +31,19 @@ class ParameterConfig(BaseModel): raise ValueError("Invalid parameter name, __reason and __is_success are reserved") return str(value) + def is_array_type(self) -> bool: + return self.type in ("array[string]", "array[number]", "array[object]") + + def element_type(self) -> Literal["string", "number", "object"]: + if self.type == "array[number]": + return "number" + elif self.type == "array[string]": + return "string" + elif self.type == "array[object]": + return "object" + else: + raise _ParameterConfigError(f"{self.type} is not array type.") + class ParameterExtractorNodeData(BaseNodeData): """ diff --git a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py index 4f31258b15..25a534256b 100644 --- a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py +++ b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py @@ -25,12 +25,15 @@ from core.prompt.advanced_prompt_transform import AdvancedPromptTransform from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate from core.prompt.simple_prompt_transform import ModelMode from core.prompt.utils.prompt_message_util import PromptMessageUtil +from core.variables.types import SegmentType from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from core.workflow.nodes.base.node import BaseNode from core.workflow.nodes.enums import NodeType -from core.workflow.nodes.llm import LLMNode, ModelConfig +from core.workflow.nodes.llm import ModelConfig, llm_utils from core.workflow.utils import variable_template_parser +from factories.variable_factory import build_segment_with_type from .entities import ParameterExtractorNodeData from .exc import ( @@ -83,7 +86,7 @@ def extract_json(text): return None -class ParameterExtractorNode(LLMNode): +class ParameterExtractorNode(BaseNode): """ Parameter Extractor Node. """ @@ -108,6 +111,10 @@ class ParameterExtractorNode(LLMNode): } } + @classmethod + def version(cls) -> str: + return "1" + def _run(self): """ Run the node. @@ -116,8 +123,11 @@ class ParameterExtractorNode(LLMNode): variable = self.graph_runtime_state.variable_pool.get(node_data.query) query = variable.text if variable else "" + variable_pool = self.graph_runtime_state.variable_pool + files = ( - self._fetch_files( + llm_utils.fetch_files( + variable_pool=variable_pool, selector=node_data.vision.configs.variable_selector, ) if node_data.vision.enabled @@ -137,7 +147,9 @@ class ParameterExtractorNode(LLMNode): raise ModelSchemaNotFoundError("Model schema not found") # fetch memory - memory = self._fetch_memory( + memory = llm_utils.fetch_memory( + variable_pool=variable_pool, + app_id=self.app_id, node_data_memory=node_data.memory, model_instance=model_instance, ) @@ -241,7 +253,12 @@ class ParameterExtractorNode(LLMNode): status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=inputs, process_data=process_data, - outputs={"__is_success": 1 if not error else 0, "__reason": error, **result}, + outputs={ + "__is_success": 1 if not error else 0, + "__reason": error, + "__usage": jsonable_encoder(usage), + **result, + }, metadata={ WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS: usage.total_tokens, WorkflowNodeExecutionMetadataKey.TOTAL_PRICE: usage.total_price, @@ -279,7 +296,7 @@ class ParameterExtractorNode(LLMNode): tool_call = invoke_result.message.tool_calls[0] if invoke_result.message.tool_calls else None # deduct quota - self.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage) + llm_utils.deduct_llm_quota(tenant_id=self.tenant_id, model_instance=model_instance, usage=usage) if text is None: text = "" @@ -578,28 +595,30 @@ class ParameterExtractorNode(LLMNode): elif parameter.type in {"string", "select"}: if isinstance(result[parameter.name], str): transformed_result[parameter.name] = result[parameter.name] - elif parameter.type.startswith("array"): + elif parameter.is_array_type(): if isinstance(result[parameter.name], list): - nested_type = parameter.type[6:-1] - transformed_result[parameter.name] = [] + nested_type = parameter.element_type() + assert nested_type is not None + segment_value = build_segment_with_type(segment_type=SegmentType(parameter.type), value=[]) + transformed_result[parameter.name] = segment_value for item in result[parameter.name]: if nested_type == "number": if isinstance(item, int | float): - transformed_result[parameter.name].append(item) + segment_value.value.append(item) elif isinstance(item, str): try: if "." in item: - transformed_result[parameter.name].append(float(item)) + segment_value.value.append(float(item)) else: - transformed_result[parameter.name].append(int(item)) + segment_value.value.append(int(item)) except ValueError: pass elif nested_type == "string": if isinstance(item, str): - transformed_result[parameter.name].append(item) + segment_value.value.append(item) elif nested_type == "object": if isinstance(item, dict): - transformed_result[parameter.name].append(item) + segment_value.value.append(item) if parameter.name not in transformed_result: if parameter.type == "number": @@ -609,7 +628,9 @@ class ParameterExtractorNode(LLMNode): elif parameter.type in {"string", "select"}: transformed_result[parameter.name] = "" elif parameter.type.startswith("array"): - transformed_result[parameter.name] = [] + transformed_result[parameter.name] = build_segment_with_type( + segment_type=SegmentType(parameter.type), value=[] + ) return transformed_result @@ -794,7 +815,9 @@ class ParameterExtractorNode(LLMNode): Fetch model config. """ if not self._model_instance or not self._model_config: - self._model_instance, self._model_config = super()._fetch_model_config(node_data_model) + self._model_instance, self._model_config = llm_utils.fetch_model_config( + tenant_id=self.tenant_id, node_data_model=node_data_model + ) return self._model_instance, self._model_config diff --git a/api/core/workflow/nodes/question_classifier/entities.py b/api/core/workflow/nodes/question_classifier/entities.py index 5219f11d26..6248df0edf 100644 --- a/api/core/workflow/nodes/question_classifier/entities.py +++ b/api/core/workflow/nodes/question_classifier/entities.py @@ -19,3 +19,12 @@ class QuestionClassifierNodeData(BaseNodeData): instruction: Optional[str] = None memory: Optional[MemoryConfig] = None vision: VisionConfig = Field(default_factory=VisionConfig) + + @property + def structured_output_enabled(self) -> bool: + # NOTE(QuantumGhost): Temporary workaround for issue #20725 + # (https://github.com/langgenius/dify/issues/20725). + # + # The proper fix would be to make `QuestionClassifierNode` inherit + # from `BaseNode` instead of `LLMNode`. + return False diff --git a/api/core/workflow/nodes/question_classifier/question_classifier_node.py b/api/core/workflow/nodes/question_classifier/question_classifier_node.py index 28d8b3b867..74024ed90c 100644 --- a/api/core/workflow/nodes/question_classifier/question_classifier_node.py +++ b/api/core/workflow/nodes/question_classifier/question_classifier_node.py @@ -18,6 +18,7 @@ from core.workflow.nodes.llm import ( LLMNode, LLMNodeChatModelMessage, LLMNodeCompletionModelPromptTemplate, + llm_utils, ) from core.workflow.utils.variable_template_parser import VariableTemplateParser from libs.json_in_md_parser import parse_and_check_json_markdown @@ -39,6 +40,10 @@ class QuestionClassifierNode(LLMNode): _node_data_cls = QuestionClassifierNodeData # type: ignore _node_type = NodeType.QUESTION_CLASSIFIER + @classmethod + def version(cls): + return "1" + def _run(self): node_data = cast(QuestionClassifierNodeData, self.node_data) variable_pool = self.graph_runtime_state.variable_pool @@ -50,7 +55,9 @@ class QuestionClassifierNode(LLMNode): # fetch model config model_instance, model_config = self._fetch_model_config(node_data.model) # fetch memory - memory = self._fetch_memory( + memory = llm_utils.fetch_memory( + variable_pool=variable_pool, + app_id=self.app_id, node_data_memory=node_data.memory, model_instance=model_instance, ) @@ -59,7 +66,8 @@ class QuestionClassifierNode(LLMNode): node_data.instruction = variable_pool.convert_template(node_data.instruction).text files = ( - self._fetch_files( + llm_utils.fetch_files( + variable_pool=variable_pool, selector=node_data.vision.configs.variable_selector, ) if node_data.vision.enabled @@ -137,7 +145,11 @@ class QuestionClassifierNode(LLMNode): "model_provider": model_config.provider, "model_name": model_config.model, } - outputs = {"class_name": category_name, "class_id": category_id} + outputs = { + "class_name": category_name, + "class_id": category_id, + "usage": jsonable_encoder(usage), + } return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, diff --git a/api/core/workflow/nodes/start/start_node.py b/api/core/workflow/nodes/start/start_node.py index 8839aec9d6..e215591888 100644 --- a/api/core/workflow/nodes/start/start_node.py +++ b/api/core/workflow/nodes/start/start_node.py @@ -10,13 +10,18 @@ class StartNode(BaseNode[StartNodeData]): _node_data_cls = StartNodeData _node_type = NodeType.START + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: node_inputs = dict(self.graph_runtime_state.variable_pool.user_inputs) - system_inputs = self.graph_runtime_state.variable_pool.system_variables + system_inputs = self.graph_runtime_state.variable_pool.system_variables.to_dict() # TODO: System variables should be directly accessible, no need for special handling # Set system variables as node outputs. for var in system_inputs: node_inputs[SYSTEM_VARIABLE_NODE_ID + "." + var] = system_inputs[var] + outputs = dict(node_inputs) - return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=node_inputs, outputs=node_inputs) + return NodeRunResult(status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=node_inputs, outputs=outputs) diff --git a/api/core/workflow/nodes/template_transform/template_transform_node.py b/api/core/workflow/nodes/template_transform/template_transform_node.py index 476cf7eee4..ba573074c3 100644 --- a/api/core/workflow/nodes/template_transform/template_transform_node.py +++ b/api/core/workflow/nodes/template_transform/template_transform_node.py @@ -28,6 +28,10 @@ class TemplateTransformNode(BaseNode[TemplateTransformNodeData]): "config": {"variables": [{"variable": "arg1", "value_selector": []}], "template": "{{ arg1 }}"}, } + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: # Get variables variables = {} diff --git a/api/core/workflow/nodes/tool/entities.py b/api/core/workflow/nodes/tool/entities.py index 21023d4ab7..691f6e0196 100644 --- a/api/core/workflow/nodes/tool/entities.py +++ b/api/core/workflow/nodes/tool/entities.py @@ -41,6 +41,10 @@ class ToolNodeData(BaseNodeData, ToolEntity): def check_type(cls, value, validation_info: ValidationInfo): typ = value value = validation_info.data.get("value") + + if value is None: + return typ + if typ == "mixed" and not isinstance(value, str): raise ValueError("value must be a string") elif typ == "variable": @@ -54,3 +58,22 @@ class ToolNodeData(BaseNodeData, ToolEntity): return typ tool_parameters: dict[str, ToolInput] + + @field_validator("tool_parameters", mode="before") + @classmethod + def filter_none_tool_inputs(cls, value): + if not isinstance(value, dict): + return value + + return { + key: tool_input + for key, tool_input in value.items() + if tool_input is not None and cls._has_valid_value(tool_input) + } + + @staticmethod + def _has_valid_value(tool_input): + """Check if the value is valid""" + if isinstance(tool_input, dict): + return tool_input.get("value") is not None + return getattr(tool_input, "value", None) is not None diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index a63585b2b0..3853a5d920 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -1,18 +1,19 @@ from collections.abc import Generator, Mapping, Sequence -from typing import Any, cast +from typing import Any, Optional, cast from sqlalchemy import select from sqlalchemy.orm import Session from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler from core.file import File, FileTransferMethod +from core.model_runtime.entities.llm_entities import LLMUsage from core.plugin.impl.exc import PluginDaemonClientSideError from core.plugin.impl.plugin import PluginInstaller from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter from core.tools.errors import ToolInvokeError from core.tools.tool_engine import ToolEngine from core.tools.utils.message_transformer import ToolFileMessageTransformer -from core.variables.segments import ArrayAnySegment +from core.variables.segments import ArrayAnySegment, ArrayFileSegment from core.variables.variables import ArrayAnyVariable from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.variable_pool import VariablePool @@ -44,6 +45,10 @@ class ToolNode(BaseNode[ToolNodeData]): _node_data_cls = ToolNodeData _node_type = NodeType.TOOL + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> Generator: """ Run the tool node @@ -62,8 +67,9 @@ class ToolNode(BaseNode[ToolNodeData]): try: from core.tools.tool_manager import ToolManager + variable_pool = self.graph_runtime_state.variable_pool if self.node_data.version != "1" else None tool_runtime = ToolManager.get_workflow_tool_runtime( - self.tenant_id, self.app_id, self.node_id, self.node_data, self.invoke_from + self.tenant_id, self.app_id, self.node_id, self.node_data, self.invoke_from, variable_pool ) except ToolNodeError as e: yield RunCompletedEvent( @@ -90,7 +96,6 @@ class ToolNode(BaseNode[ToolNodeData]): node_data=self.node_data, for_log=True, ) - # get conversation id conversation_id = self.graph_runtime_state.variable_pool.get(["sys", SystemVariableKey.CONVERSATION_ID]) @@ -163,7 +168,9 @@ class ToolNode(BaseNode[ToolNodeData]): if tool_input.type == "variable": variable = variable_pool.get(tool_input.value) if variable is None: - raise ToolParameterError(f"Variable {tool_input.value} does not exist") + if parameter.required: + raise ToolParameterError(f"Variable {tool_input.value} does not exist") + continue parameter_value = variable.value elif tool_input.type in {"mixed", "constant"}: segment_group = variable_pool.convert_template(str(tool_input.value)) @@ -184,6 +191,7 @@ class ToolNode(BaseNode[ToolNodeData]): messages: Generator[ToolInvokeMessage, None, None], tool_info: Mapping[str, Any], parameters_for_log: dict[str, Any], + agent_thoughts: Optional[list] = None, ) -> Generator: """ Convert ToolInvokeMessages into tuple[plain_text, files] @@ -202,7 +210,7 @@ class ToolNode(BaseNode[ToolNodeData]): agent_logs: list[AgentLogEvent] = [] agent_execution_metadata: Mapping[WorkflowNodeExecutionMetadataKey, Any] = {} - + llm_usage: LLMUsage | None = None variables: dict[str, Any] = {} for message in message_stream: @@ -270,13 +278,15 @@ class ToolNode(BaseNode[ToolNodeData]): elif message.type == ToolInvokeMessage.MessageType.JSON: assert isinstance(message.message, ToolInvokeMessage.JsonMessage) if self.node_type == NodeType.AGENT: - msg_metadata = message.message.json_object.pop("execution_metadata", {}) + msg_metadata: dict[str, Any] = message.message.json_object.pop("execution_metadata", {}) + llm_usage = LLMUsage.from_metadata(msg_metadata) agent_execution_metadata = { - key: value + WorkflowNodeExecutionMetadataKey(key): value for key, value in msg_metadata.items() if key in WorkflowNodeExecutionMetadataKey.__members__.values() } - json.append(message.message.json_object) + if message.message.json_object is not None: + json.append(message.message.json_object) elif message.type == ToolInvokeMessage.MessageType.LINK: assert isinstance(message.message, ToolInvokeMessage.TextMessage) stream_text = f"Link: {message.message.text}\n" @@ -300,6 +310,7 @@ class ToolNode(BaseNode[ToolNodeData]): variables[variable_name] = variable_value elif message.type == ToolInvokeMessage.MessageType.FILE: assert message.meta is not None + assert isinstance(message.meta, File) files.append(message.meta["file"]) elif message.type == ToolInvokeMessage.MessageType.LOG: assert isinstance(message.message, ToolInvokeMessage.LogMessage) @@ -318,6 +329,7 @@ class ToolNode(BaseNode[ToolNodeData]): icon = current_plugin.declaration.icon except StopIteration: pass + icon_dark = None try: builtin_tool = next( provider @@ -328,10 +340,12 @@ class ToolNode(BaseNode[ToolNodeData]): if provider.name == dict_metadata["provider"] ) icon = builtin_tool.icon + icon_dark = builtin_tool.icon_dark except StopIteration: pass dict_metadata["icon"] = icon + dict_metadata["icon_dark"] = icon_dark message.message.metadata = dict_metadata agent_log = AgentLogEvent( id=message.message.id, @@ -366,16 +380,41 @@ class ToolNode(BaseNode[ToolNodeData]): context=message.message.context, ) + # Add agent_logs to outputs['json'] to ensure frontend can access thinking process + json_output: list[dict[str, Any]] = [] + + # Step 1: append each agent log as its own dict. + if agent_logs: + for log in agent_logs: + json_output.append( + { + "id": log.id, + "parent_id": log.parent_id, + "error": log.error, + "status": log.status, + "data": log.data, + "label": log.label, + "metadata": log.metadata, + "node_id": log.node_id, + } + ) + # Step 2: normalize JSON into {"data": [...]}.change json to list[dict] + if json: + json_output.extend(json) + else: + json_output.append({"data": []}) + yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, - outputs={"text": text, "files": files, "json": json, **variables}, + outputs={"text": text, "files": ArrayFileSegment(value=files), "json": json_output, **variables}, metadata={ **agent_execution_metadata, WorkflowNodeExecutionMetadataKey.TOOL_INFO: tool_info, WorkflowNodeExecutionMetadataKey.AGENT_LOG: agent_logs, }, inputs=parameters_for_log, + llm_usage=llm_usage, ) ) diff --git a/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py b/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py index db3e25b015..96bb3e793a 100644 --- a/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py +++ b/api/core/workflow/nodes/variable_aggregator/variable_aggregator_node.py @@ -1,3 +1,6 @@ +from collections.abc import Mapping + +from core.variables.segments import Segment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.base import BaseNode @@ -9,16 +12,20 @@ class VariableAggregatorNode(BaseNode[VariableAssignerNodeData]): _node_data_cls = VariableAssignerNodeData _node_type = NodeType.VARIABLE_AGGREGATOR + @classmethod + def version(cls) -> str: + return "1" + def _run(self) -> NodeRunResult: # Get variables - outputs = {} + outputs: dict[str, Segment | Mapping[str, Segment]] = {} inputs = {} if not self.node_data.advanced_settings or not self.node_data.advanced_settings.group_enabled: for selector in self.node_data.variables: variable = self.graph_runtime_state.variable_pool.get(selector) if variable is not None: - outputs = {"output": variable.to_object()} + outputs = {"output": variable} inputs = {".".join(selector[1:]): variable.to_object()} break @@ -28,7 +35,7 @@ class VariableAggregatorNode(BaseNode[VariableAssignerNodeData]): variable = self.graph_runtime_state.variable_pool.get(selector) if variable is not None: - outputs[group.group_name] = {"output": variable.to_object()} + outputs[group.group_name] = {"output": variable} inputs[".".join(selector[1:])] = variable.to_object() break diff --git a/api/core/workflow/nodes/variable_assigner/common/helpers.py b/api/core/workflow/nodes/variable_assigner/common/helpers.py index 8031b57fa8..0d2822233e 100644 --- a/api/core/workflow/nodes/variable_assigner/common/helpers.py +++ b/api/core/workflow/nodes/variable_assigner/common/helpers.py @@ -1,19 +1,55 @@ -from sqlalchemy import select -from sqlalchemy.orm import Session +from collections.abc import Mapping, MutableMapping, Sequence +from typing import Any, TypeVar -from core.variables import Variable -from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError -from extensions.ext_database import db -from models import ConversationVariable +from pydantic import BaseModel +from core.variables import Segment +from core.variables.consts import MIN_SELECTORS_LENGTH +from core.variables.types import SegmentType -def update_conversation_variable(conversation_id: str, variable: Variable): - stmt = select(ConversationVariable).where( - ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id +# Use double underscore (`__`) prefix for internal variables +# to minimize risk of collision with user-defined variable names. +_UPDATED_VARIABLES_KEY = "__updated_variables" + + +class UpdatedVariable(BaseModel): + name: str + selector: Sequence[str] + value_type: SegmentType + new_value: Any + + +_T = TypeVar("_T", bound=MutableMapping[str, Any]) + + +def variable_to_processed_data(selector: Sequence[str], seg: Segment) -> UpdatedVariable: + if len(selector) < MIN_SELECTORS_LENGTH: + raise Exception("selector too short") + node_id, var_name = selector[:2] + return UpdatedVariable( + name=var_name, + selector=list(selector[:2]), + value_type=seg.value_type, + new_value=seg.value, ) - with Session(db.engine) as session: - row = session.scalar(stmt) - if not row: - raise VariableOperatorNodeError("conversation variable not found in the database") - row.data = variable.model_dump_json() - session.commit() + + +def set_updated_variables(m: _T, updates: Sequence[UpdatedVariable]) -> _T: + m[_UPDATED_VARIABLES_KEY] = updates + return m + + +def get_updated_variables(m: Mapping[str, Any]) -> Sequence[UpdatedVariable] | None: + updated_values = m.get(_UPDATED_VARIABLES_KEY, None) + if updated_values is None: + return None + result = [] + for items in updated_values: + if isinstance(items, UpdatedVariable): + result.append(items) + elif isinstance(items, dict): + items = UpdatedVariable.model_validate(items) + result.append(items) + else: + raise TypeError(f"Invalid updated variable: {items}, type={type(items)}") + return result diff --git a/api/core/workflow/nodes/variable_assigner/common/impl.py b/api/core/workflow/nodes/variable_assigner/common/impl.py new file mode 100644 index 0000000000..8f7a44bb62 --- /dev/null +++ b/api/core/workflow/nodes/variable_assigner/common/impl.py @@ -0,0 +1,38 @@ +from sqlalchemy import Engine, select +from sqlalchemy.orm import Session + +from core.variables.variables import Variable +from models.engine import db +from models.workflow import ConversationVariable + +from .exc import VariableOperatorNodeError + + +class ConversationVariableUpdaterImpl: + _engine: Engine | None + + def __init__(self, engine: Engine | None = None) -> None: + self._engine = engine + + def _get_engine(self) -> Engine: + if self._engine: + return self._engine + return db.engine + + def update(self, conversation_id: str, variable: Variable): + stmt = select(ConversationVariable).where( + ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id + ) + with Session(self._get_engine()) as session: + row = session.scalar(stmt) + if not row: + raise VariableOperatorNodeError("conversation variable not found in the database") + row.data = variable.model_dump_json() + session.commit() + + def flush(self): + pass + + +def conversation_variable_updater_factory() -> ConversationVariableUpdaterImpl: + return ConversationVariableUpdaterImpl() diff --git a/api/core/workflow/nodes/variable_assigner/v1/node.py b/api/core/workflow/nodes/variable_assigner/v1/node.py index 835e1d77b5..1864b13784 100644 --- a/api/core/workflow/nodes/variable_assigner/v1/node.py +++ b/api/core/workflow/nodes/variable_assigner/v1/node.py @@ -1,4 +1,9 @@ +from collections.abc import Callable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Optional, TypeAlias + from core.variables import SegmentType, Variable +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID +from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.base import BaseNode @@ -7,16 +12,71 @@ from core.workflow.nodes.variable_assigner.common import helpers as common_helpe from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError from factories import variable_factory +from ..common.impl import conversation_variable_updater_factory from .node_data import VariableAssignerData, WriteMode +if TYPE_CHECKING: + from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState + + +_CONV_VAR_UPDATER_FACTORY: TypeAlias = Callable[[], ConversationVariableUpdater] + class VariableAssignerNode(BaseNode[VariableAssignerData]): _node_data_cls = VariableAssignerData _node_type = NodeType.VARIABLE_ASSIGNER + _conv_var_updater_factory: _CONV_VAR_UPDATER_FACTORY + + def __init__( + self, + id: str, + config: Mapping[str, Any], + graph_init_params: "GraphInitParams", + graph: "Graph", + graph_runtime_state: "GraphRuntimeState", + previous_node_id: Optional[str] = None, + thread_pool_id: Optional[str] = None, + conv_var_updater_factory: _CONV_VAR_UPDATER_FACTORY = conversation_variable_updater_factory, + ) -> None: + super().__init__( + id=id, + config=config, + graph_init_params=graph_init_params, + graph=graph, + graph_runtime_state=graph_runtime_state, + previous_node_id=previous_node_id, + thread_pool_id=thread_pool_id, + ) + self._conv_var_updater_factory = conv_var_updater_factory + + @classmethod + def version(cls) -> str: + return "1" + + @classmethod + def _extract_variable_selector_to_variable_mapping( + cls, + *, + graph_config: Mapping[str, Any], + node_id: str, + node_data: VariableAssignerData, + ) -> Mapping[str, Sequence[str]]: + mapping = {} + assigned_variable_node_id = node_data.assigned_variable_selector[0] + if assigned_variable_node_id == CONVERSATION_VARIABLE_NODE_ID: + selector_key = ".".join(node_data.assigned_variable_selector) + key = f"{node_id}.#{selector_key}#" + mapping[key] = node_data.assigned_variable_selector + + selector_key = ".".join(node_data.input_variable_selector) + key = f"{node_id}.#{selector_key}#" + mapping[key] = node_data.input_variable_selector + return mapping def _run(self) -> NodeRunResult: + assigned_variable_selector = self.node_data.assigned_variable_selector # Should be String, Number, Object, ArrayString, ArrayNumber, ArrayObject - original_variable = self.graph_runtime_state.variable_pool.get(self.node_data.assigned_variable_selector) + original_variable = self.graph_runtime_state.variable_pool.get(assigned_variable_selector) if not isinstance(original_variable, Variable): raise VariableOperatorNodeError("assigned variable not found") @@ -44,24 +104,33 @@ class VariableAssignerNode(BaseNode[VariableAssignerData]): raise VariableOperatorNodeError(f"unsupported write mode: {self.node_data.write_mode}") # Over write the variable. - self.graph_runtime_state.variable_pool.add(self.node_data.assigned_variable_selector, updated_variable) + self.graph_runtime_state.variable_pool.add(assigned_variable_selector, updated_variable) # TODO: Move database operation to the pipeline. # Update conversation variable. conversation_id = self.graph_runtime_state.variable_pool.get(["sys", "conversation_id"]) if not conversation_id: raise VariableOperatorNodeError("conversation_id not found") - common_helpers.update_conversation_variable(conversation_id=conversation_id.text, variable=updated_variable) + conv_var_updater = self._conv_var_updater_factory() + conv_var_updater.update(conversation_id=conversation_id.text, variable=updated_variable) + conv_var_updater.flush() + updated_variables = [common_helpers.variable_to_processed_data(assigned_variable_selector, updated_variable)] return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs={ "value": income_value.to_object(), }, + # NOTE(QuantumGhost): although only one variable is updated in `v1.VariableAssignerNode`, + # we still set `output_variables` as a list to ensure the schema of output is + # compatible with `v2.VariableAssignerNode`. + process_data=common_helpers.set_updated_variables({}, updated_variables), + outputs={}, ) def get_zero_value(t: SegmentType): + # TODO(QuantumGhost): this should be a method of `SegmentType`. match t: case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER: return variable_factory.build_segment([]) @@ -69,6 +138,10 @@ def get_zero_value(t: SegmentType): return variable_factory.build_segment({}) case SegmentType.STRING: return variable_factory.build_segment("") + case SegmentType.INTEGER: + return variable_factory.build_segment(0) + case SegmentType.FLOAT: + return variable_factory.build_segment(0.0) case SegmentType.NUMBER: return variable_factory.build_segment(0) case _: diff --git a/api/core/workflow/nodes/variable_assigner/v2/constants.py b/api/core/workflow/nodes/variable_assigner/v2/constants.py index 3797bfa77a..7f760e5baa 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/constants.py +++ b/api/core/workflow/nodes/variable_assigner/v2/constants.py @@ -1,5 +1,6 @@ from core.variables import SegmentType +# Note: This mapping is duplicated with `get_zero_value`. Consider refactoring to avoid redundancy. EMPTY_VALUE_MAPPING = { SegmentType.STRING: "", SegmentType.NUMBER: 0, diff --git a/api/core/workflow/nodes/variable_assigner/v2/entities.py b/api/core/workflow/nodes/variable_assigner/v2/entities.py index 01df33b6d4..d93affcd15 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/entities.py +++ b/api/core/workflow/nodes/variable_assigner/v2/entities.py @@ -12,6 +12,12 @@ class VariableOperationItem(BaseModel): variable_selector: Sequence[str] input_type: InputType operation: Operation + # NOTE(QuantumGhost): The `value` field serves multiple purposes depending on context: + # + # 1. For CONSTANT input_type: Contains the literal value to be used in the operation. + # 2. For VARIABLE input_type: Initially contains the selector of the source variable. + # 3. During the variable updating procedure: The `value` field is reassigned to hold + # the resolved actual value that will be applied to the target variable. value: Any | None = None diff --git a/api/core/workflow/nodes/variable_assigner/v2/exc.py b/api/core/workflow/nodes/variable_assigner/v2/exc.py index b67af6d73c..fd6c304a9a 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/exc.py +++ b/api/core/workflow/nodes/variable_assigner/v2/exc.py @@ -29,3 +29,8 @@ class InvalidInputValueError(VariableOperatorNodeError): class ConversationIDNotFoundError(VariableOperatorNodeError): def __init__(self): super().__init__("conversation_id not found") + + +class InvalidDataError(VariableOperatorNodeError): + def __init__(self, message: str) -> None: + super().__init__(message) diff --git a/api/core/workflow/nodes/variable_assigner/v2/helpers.py b/api/core/workflow/nodes/variable_assigner/v2/helpers.py index 8fb2a27388..7a20975b15 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/helpers.py +++ b/api/core/workflow/nodes/variable_assigner/v2/helpers.py @@ -10,10 +10,16 @@ def is_operation_supported(*, variable_type: SegmentType, operation: Operation): case Operation.OVER_WRITE | Operation.CLEAR: return True case Operation.SET: - return variable_type in {SegmentType.OBJECT, SegmentType.STRING, SegmentType.NUMBER} + return variable_type in { + SegmentType.OBJECT, + SegmentType.STRING, + SegmentType.NUMBER, + SegmentType.INTEGER, + SegmentType.FLOAT, + } case Operation.ADD | Operation.SUBTRACT | Operation.MULTIPLY | Operation.DIVIDE: # Only number variable can be added, subtracted, multiplied or divided - return variable_type == SegmentType.NUMBER + return variable_type in {SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT} case Operation.APPEND | Operation.EXTEND: # Only array variable can be appended or extended return variable_type in { @@ -46,7 +52,7 @@ def is_constant_input_supported(*, variable_type: SegmentType, operation: Operat match variable_type: case SegmentType.STRING | SegmentType.OBJECT: return operation in {Operation.OVER_WRITE, Operation.SET} - case SegmentType.NUMBER: + case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: return operation in { Operation.OVER_WRITE, Operation.SET, @@ -66,7 +72,7 @@ def is_input_value_valid(*, variable_type: SegmentType, operation: Operation, va case SegmentType.STRING: return isinstance(value, str) - case SegmentType.NUMBER: + case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: if not isinstance(value, int | float): return False if operation == Operation.DIVIDE and value == 0: diff --git a/api/core/workflow/nodes/variable_assigner/v2/node.py b/api/core/workflow/nodes/variable_assigner/v2/node.py index 8759a55b34..9292da6f1c 100644 --- a/api/core/workflow/nodes/variable_assigner/v2/node.py +++ b/api/core/workflow/nodes/variable_assigner/v2/node.py @@ -1,34 +1,84 @@ import json -from collections.abc import Sequence -from typing import Any, cast +from collections.abc import Callable, Mapping, MutableMapping, Sequence +from typing import Any, TypeAlias, cast from core.app.entities.app_invoke_entities import InvokeFrom from core.variables import SegmentType, Variable +from core.variables.consts import MIN_SELECTORS_LENGTH from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID +from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus from core.workflow.nodes.base import BaseNode from core.workflow.nodes.enums import NodeType from core.workflow.nodes.variable_assigner.common import helpers as common_helpers from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError +from core.workflow.nodes.variable_assigner.common.impl import conversation_variable_updater_factory from . import helpers from .constants import EMPTY_VALUE_MAPPING -from .entities import VariableAssignerNodeData +from .entities import VariableAssignerNodeData, VariableOperationItem from .enums import InputType, Operation from .exc import ( ConversationIDNotFoundError, InputTypeNotSupportedError, + InvalidDataError, InvalidInputValueError, OperationNotSupportedError, VariableNotFoundError, ) +_CONV_VAR_UPDATER_FACTORY: TypeAlias = Callable[[], ConversationVariableUpdater] + + +def _target_mapping_from_item(mapping: MutableMapping[str, Sequence[str]], node_id: str, item: VariableOperationItem): + selector_node_id = item.variable_selector[0] + if selector_node_id != CONVERSATION_VARIABLE_NODE_ID: + return + selector_str = ".".join(item.variable_selector) + key = f"{node_id}.#{selector_str}#" + mapping[key] = item.variable_selector + + +def _source_mapping_from_item(mapping: MutableMapping[str, Sequence[str]], node_id: str, item: VariableOperationItem): + # Keep this in sync with the logic in _run methods... + if item.input_type != InputType.VARIABLE: + return + selector = item.value + if not isinstance(selector, list): + raise InvalidDataError(f"selector is not a list, {node_id=}, {item=}") + if len(selector) < MIN_SELECTORS_LENGTH: + raise InvalidDataError(f"selector too short, {node_id=}, {item=}") + selector_str = ".".join(selector) + key = f"{node_id}.#{selector_str}#" + mapping[key] = selector + class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): _node_data_cls = VariableAssignerNodeData _node_type = NodeType.VARIABLE_ASSIGNER + def _conv_var_updater_factory(self) -> ConversationVariableUpdater: + return conversation_variable_updater_factory() + + @classmethod + def version(cls) -> str: + return "2" + + @classmethod + def _extract_variable_selector_to_variable_mapping( + cls, + *, + graph_config: Mapping[str, Any], + node_id: str, + node_data: VariableAssignerNodeData, + ) -> Mapping[str, Sequence[str]]: + var_mapping: dict[str, Sequence[str]] = {} + for item in node_data.items: + _target_mapping_from_item(var_mapping, node_id, item) + _source_mapping_from_item(var_mapping, node_id, item) + return var_mapping + def _run(self) -> NodeRunResult: inputs = self.node_data.model_dump() process_data: dict[str, Any] = {} @@ -114,6 +164,7 @@ class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): # remove the duplicated items first. updated_variable_selectors = list(set(map(tuple, updated_variable_selectors))) + conv_var_updater = self._conv_var_updater_factory() # Update variables for selector in updated_variable_selectors: variable = self.graph_runtime_state.variable_pool.get(selector) @@ -128,15 +179,23 @@ class VariableAssignerNode(BaseNode[VariableAssignerNodeData]): raise ConversationIDNotFoundError else: conversation_id = conversation_id.value - common_helpers.update_conversation_variable( + conv_var_updater.update( conversation_id=cast(str, conversation_id), variable=variable, ) - + conv_var_updater.flush() + updated_variables = [ + common_helpers.variable_to_processed_data(selector, seg) + for selector in updated_variable_selectors + if (seg := self.graph_runtime_state.variable_pool.get(selector)) is not None + ] + + process_data = common_helpers.set_updated_variables(process_data, updated_variables) return NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, inputs=inputs, process_data=process_data, + outputs={}, ) def _handle_item( diff --git a/api/core/workflow/repositories/draft_variable_repository.py b/api/core/workflow/repositories/draft_variable_repository.py new file mode 100644 index 0000000000..cadc23f845 --- /dev/null +++ b/api/core/workflow/repositories/draft_variable_repository.py @@ -0,0 +1,32 @@ +import abc +from collections.abc import Mapping +from typing import Any, Protocol + +from sqlalchemy.orm import Session + +from core.workflow.nodes.enums import NodeType + + +class DraftVariableSaver(Protocol): + @abc.abstractmethod + def save(self, process_data: Mapping[str, Any] | None, outputs: Mapping[str, Any] | None): + pass + + +class DraftVariableSaverFactory(Protocol): + @abc.abstractmethod + def __call__( + self, + session: Session, + app_id: str, + node_id: str, + node_type: NodeType, + node_execution_id: str, + enclosing_node_id: str | None = None, + ) -> "DraftVariableSaver": + pass + + +class NoopDraftVariableSaver(DraftVariableSaver): + def save(self, process_data: Mapping[str, Any] | None, outputs: Mapping[str, Any] | None): + pass diff --git a/api/core/workflow/system_variable.py b/api/core/workflow/system_variable.py new file mode 100644 index 0000000000..df90c16596 --- /dev/null +++ b/api/core/workflow/system_variable.py @@ -0,0 +1,89 @@ +from collections.abc import Sequence +from typing import Any + +from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator + +from core.file.models import File +from core.workflow.enums import SystemVariableKey + + +class SystemVariable(BaseModel): + """A model for managing system variables. + + Fields with a value of `None` are treated as absent and will not be included + in the variable pool. + """ + + model_config = ConfigDict( + extra="forbid", + serialize_by_alias=True, + validate_by_alias=True, + ) + + user_id: str | None = None + + # Ideally, `app_id` and `workflow_id` should be required and not `None`. + # However, there are scenarios in the codebase where these fields are not set. + # To maintain compatibility, they are marked as optional here. + app_id: str | None = None + workflow_id: str | None = None + + files: Sequence[File] = Field(default_factory=list) + + # NOTE: The `workflow_execution_id` field was previously named `workflow_run_id`. + # To maintain compatibility with existing workflows, it must be serialized + # as `workflow_run_id` in dictionaries or JSON objects, and also referenced + # as `workflow_run_id` in the variable pool. + workflow_execution_id: str | None = Field( + validation_alias=AliasChoices("workflow_execution_id", "workflow_run_id"), + serialization_alias="workflow_run_id", + default=None, + ) + # Chatflow related fields. + query: str | None = None + conversation_id: str | None = None + dialogue_count: int | None = None + + @model_validator(mode="before") + @classmethod + def validate_json_fields(cls, data): + if isinstance(data, dict): + # For JSON validation, only allow workflow_run_id + if "workflow_execution_id" in data and "workflow_run_id" not in data: + # This is likely from direct instantiation, allow it + return data + elif "workflow_execution_id" in data and "workflow_run_id" in data: + # Both present, remove workflow_execution_id + data = data.copy() + data.pop("workflow_execution_id") + return data + return data + + @classmethod + def empty(cls) -> "SystemVariable": + return cls() + + def to_dict(self) -> dict[SystemVariableKey, Any]: + # NOTE: This method is provided for compatibility with legacy code. + # New code should use the `SystemVariable` object directly instead of converting + # it to a dictionary, as this conversion results in the loss of type information + # for each key, making static analysis more difficult. + + d: dict[SystemVariableKey, Any] = { + SystemVariableKey.FILES: self.files, + } + if self.user_id is not None: + d[SystemVariableKey.USER_ID] = self.user_id + if self.app_id is not None: + d[SystemVariableKey.APP_ID] = self.app_id + if self.workflow_id is not None: + d[SystemVariableKey.WORKFLOW_ID] = self.workflow_id + if self.workflow_execution_id is not None: + d[SystemVariableKey.WORKFLOW_EXECUTION_ID] = self.workflow_execution_id + if self.query is not None: + d[SystemVariableKey.QUERY] = self.query + if self.conversation_id is not None: + d[SystemVariableKey.CONVERSATION_ID] = self.conversation_id + if self.dialogue_count is not None: + d[SystemVariableKey.DIALOGUE_COUNT] = self.dialogue_count + return d diff --git a/api/core/workflow/utils/structured_output/entities.py b/api/core/workflow/utils/structured_output/entities.py deleted file mode 100644 index 6491042bfe..0000000000 --- a/api/core/workflow/utils/structured_output/entities.py +++ /dev/null @@ -1,16 +0,0 @@ -from enum import StrEnum - - -class ResponseFormat(StrEnum): - """Constants for model response formats""" - - JSON_SCHEMA = "json_schema" # model's structured output mode. some model like gemini, gpt-4o, support this mode. - JSON = "JSON" # model's json mode. some model like claude support this mode. - JSON_OBJECT = "json_object" # json mode's another alias. some model like deepseek-chat, qwen use this alias. - - -class SpecialModelType(StrEnum): - """Constants for identifying model types""" - - GEMINI = "gemini" - OLLAMA = "ollama" diff --git a/api/core/workflow/utils/structured_output/prompt.py b/api/core/workflow/utils/structured_output/prompt.py deleted file mode 100644 index 06d9b2056e..0000000000 --- a/api/core/workflow/utils/structured_output/prompt.py +++ /dev/null @@ -1,17 +0,0 @@ -STRUCTURED_OUTPUT_PROMPT = """You’re a helpful AI assistant. You could answer questions and output in JSON format. -constraints: - - You must output in JSON format. - - Do not output boolean value, use string type instead. - - Do not output integer or float value, use number type instead. -eg: - Here is the JSON schema: - {"additionalProperties": false, "properties": {"age": {"type": "number"}, "name": {"type": "string"}}, "required": ["name", "age"], "type": "object"} - - Here is the user's question: - My name is John Doe and I am 30 years old. - - output: - {"name": "John Doe", "age": 30} -Here is the JSON schema: -{{schema}} -""" # noqa: E501 diff --git a/api/core/workflow/utils/variable_utils.py b/api/core/workflow/utils/variable_utils.py new file mode 100644 index 0000000000..868868315b --- /dev/null +++ b/api/core/workflow/utils/variable_utils.py @@ -0,0 +1,29 @@ +from core.variables.segments import ObjectSegment, Segment +from core.workflow.entities.variable_pool import VariablePool, VariableValue + + +def append_variables_recursively( + pool: VariablePool, node_id: str, variable_key_list: list[str], variable_value: VariableValue | Segment +): + """ + Append variables recursively + :param pool: variable pool to append variables to + :param node_id: node id + :param variable_key_list: variable key list + :param variable_value: variable value + :return: + """ + pool.add([node_id] + variable_key_list, variable_value) + + # if variable_value is a dict, then recursively append variables + if isinstance(variable_value, ObjectSegment): + variable_dict = variable_value.value + elif isinstance(variable_value, dict): + variable_dict = variable_value + else: + return + + for key, value in variable_dict.items(): + # construct new key list + new_key_list = variable_key_list + [key] + append_variables_recursively(pool, node_id=node_id, variable_key_list=new_key_list, variable_value=value) diff --git a/api/core/workflow/variable_loader.py b/api/core/workflow/variable_loader.py new file mode 100644 index 0000000000..1e13871d0a --- /dev/null +++ b/api/core/workflow/variable_loader.py @@ -0,0 +1,84 @@ +import abc +from collections.abc import Mapping, Sequence +from typing import Any, Protocol + +from core.variables import Variable +from core.variables.consts import MIN_SELECTORS_LENGTH +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.utils import variable_utils + + +class VariableLoader(Protocol): + """Interface for loading variables based on selectors. + + A `VariableLoader` is responsible for retrieving additional variables required during the execution + of a single node, which are not provided as user inputs. + + NOTE(QuantumGhost): Typically, all variables loaded by a `VariableLoader` should belong to the same + application and share the same `app_id`. However, this interface does not enforce that constraint, + and the `app_id` parameter is intentionally omitted from `load_variables` to achieve separation of + concern and allow for flexible implementations. + + Implementations of `VariableLoader` should almost always have an `app_id` parameter in + their constructor. + + TODO(QuantumGhost): this is a temporally workaround. If we can move the creation of node instance into + `WorkflowService.single_step_run`, we may get rid of this interface. + """ + + @abc.abstractmethod + def load_variables(self, selectors: list[list[str]]) -> list[Variable]: + """Load variables based on the provided selectors. If the selectors are empty, + this method should return an empty list. + + The order of the returned variables is not guaranteed. If the caller wants to ensure + a specific order, they should sort the returned list themselves. + + :param: selectors: a list of string list, each inner list should have at least two elements: + - the first element is the node ID, + - the second element is the variable name. + :return: a list of Variable objects that match the provided selectors. + """ + pass + + +class _DummyVariableLoader(VariableLoader): + """A dummy implementation of VariableLoader that does not load any variables. + Serves as a placeholder when no variable loading is needed. + """ + + def load_variables(self, selectors: list[list[str]]) -> list[Variable]: + return [] + + +DUMMY_VARIABLE_LOADER = _DummyVariableLoader() + + +def load_into_variable_pool( + variable_loader: VariableLoader, + variable_pool: VariablePool, + variable_mapping: Mapping[str, Sequence[str]], + user_inputs: Mapping[str, Any], +): + # Loading missing variable from draft var here, and set it into + # variable_pool. + variables_to_load: list[list[str]] = [] + for key, selector in variable_mapping.items(): + # NOTE(QuantumGhost): this logic needs to be in sync with + # `WorkflowEntry.mapping_user_inputs_to_variable_pool`. + node_variable_list = key.split(".") + if len(node_variable_list) < 1: + raise ValueError(f"Invalid variable key: {key}. It should have at least one element.") + if key in user_inputs: + continue + node_variable_key = ".".join(node_variable_list[1:]) + if node_variable_key in user_inputs: + continue + if variable_pool.get(selector) is None: + variables_to_load.append(list(selector)) + loaded = variable_loader.load_variables(variables_to_load) + for var in loaded: + assert len(var.selector) >= MIN_SELECTORS_LENGTH, f"Invalid variable {var}" + variable_utils.append_variables_recursively( + variable_pool, node_id=var.selector[0], variable_key_list=list(var.selector[1:]), variable_value=var + ) diff --git a/api/core/workflow/workflow_cycle_manager.py b/api/core/workflow/workflow_cycle_manager.py index b88f9edd03..50ff733979 100644 --- a/api/core/workflow/workflow_cycle_manager.py +++ b/api/core/workflow/workflow_cycle_manager.py @@ -26,7 +26,9 @@ from core.workflow.entities.workflow_node_execution import ( from core.workflow.enums import SystemVariableKey from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.system_variable import SystemVariable from core.workflow.workflow_entry import WorkflowEntry +from libs.datetime_utils import naive_utc_now @dataclass @@ -42,7 +44,7 @@ class WorkflowCycleManager: self, *, application_generate_entity: Union[AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity], - workflow_system_variables: dict[SystemVariableKey, Any], + workflow_system_variables: SystemVariable, workflow_info: CycleManagerWorkflowInfo, workflow_execution_repository: WorkflowExecutionRepository, workflow_node_execution_repository: WorkflowNodeExecutionRepository, @@ -55,17 +57,22 @@ class WorkflowCycleManager: def handle_workflow_run_start(self) -> WorkflowExecution: inputs = {**self._application_generate_entity.inputs} - for key, value in (self._workflow_system_variables or {}).items(): - if key.value == "conversation": - continue - inputs[f"sys.{key.value}"] = value + + # Iterate over SystemVariable fields using Pydantic's model_fields + if self._workflow_system_variables: + for field_name, value in self._workflow_system_variables.to_dict().items(): + if field_name == SystemVariableKey.CONVERSATION_ID: + continue + inputs[f"sys.{field_name}"] = value # handle special values inputs = dict(WorkflowEntry.handle_special_values(inputs) or {}) # init workflow run # TODO: This workflow_run_id should always not be None, maybe we can use a more elegant way to handle this - execution_id = str(self._workflow_system_variables.get(SystemVariableKey.WORKFLOW_EXECUTION_ID) or uuid4()) + execution_id = str( + self._workflow_system_variables.workflow_execution_id if self._workflow_system_variables else None + ) or str(uuid4()) execution = WorkflowExecution.new( id_=execution_id, workflow_id=self._workflow_info.workflow_id, @@ -92,7 +99,7 @@ class WorkflowCycleManager: ) -> WorkflowExecution: workflow_execution = self._get_workflow_execution_or_raise_error(workflow_run_id) - outputs = WorkflowEntry.handle_special_values(outputs) + # outputs = WorkflowEntry.handle_special_values(outputs) workflow_execution.status = WorkflowExecutionStatus.SUCCEEDED workflow_execution.outputs = outputs or {} @@ -125,7 +132,7 @@ class WorkflowCycleManager: trace_manager: Optional[TraceQueueManager] = None, ) -> WorkflowExecution: execution = self._get_workflow_execution_or_raise_error(workflow_run_id) - outputs = WorkflowEntry.handle_special_values(dict(outputs) if outputs else None) + # outputs = WorkflowEntry.handle_special_values(dict(outputs) if outputs else None) execution.status = WorkflowExecutionStatus.PARTIAL_SUCCEEDED execution.outputs = outputs or {} @@ -160,12 +167,13 @@ class WorkflowCycleManager: exceptions_count: int = 0, ) -> WorkflowExecution: workflow_execution = self._get_workflow_execution_or_raise_error(workflow_run_id) + now = naive_utc_now() workflow_execution.status = WorkflowExecutionStatus(status.value) workflow_execution.error_message = error_message workflow_execution.total_tokens = total_tokens workflow_execution.total_steps = total_steps - workflow_execution.finished_at = datetime.now(UTC).replace(tzinfo=None) + workflow_execution.finished_at = now workflow_execution.exceptions_count = exceptions_count # Use the instance repository to find running executions for a workflow run @@ -174,7 +182,6 @@ class WorkflowCycleManager: ) # Update the domain models - now = datetime.now(UTC).replace(tzinfo=None) for node_execution in running_node_executions: if node_execution.node_execution_id: # Update the domain model @@ -242,9 +249,9 @@ class WorkflowCycleManager: raise ValueError(f"Domain node execution not found: {event.node_execution_id}") # Process data - inputs = WorkflowEntry.handle_special_values(event.inputs) - process_data = WorkflowEntry.handle_special_values(event.process_data) - outputs = WorkflowEntry.handle_special_values(event.outputs) + inputs = event.inputs + process_data = event.process_data + outputs = event.outputs # Convert metadata keys to strings execution_metadata_dict = {} @@ -289,7 +296,7 @@ class WorkflowCycleManager: # Process data inputs = WorkflowEntry.handle_special_values(event.inputs) process_data = WorkflowEntry.handle_special_values(event.process_data) - outputs = WorkflowEntry.handle_special_values(event.outputs) + outputs = event.outputs # Convert metadata keys to strings execution_metadata_dict = {} @@ -326,7 +333,7 @@ class WorkflowCycleManager: finished_at = datetime.now(UTC).replace(tzinfo=None) elapsed_time = (finished_at - created_at).total_seconds() inputs = WorkflowEntry.handle_special_values(event.inputs) - outputs = WorkflowEntry.handle_special_values(event.outputs) + outputs = event.outputs # Convert metadata keys to strings origin_metadata = { diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index 7648947fca..1399efcdb1 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -21,6 +21,8 @@ from core.workflow.nodes import NodeType from core.workflow.nodes.base import BaseNode from core.workflow.nodes.event import NodeEvent from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING +from core.workflow.system_variable import SystemVariable +from core.workflow.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from factories import file_factory from models.enums import UserFrom from models.workflow import ( @@ -68,6 +70,7 @@ class WorkflowEntry: raise ValueError("Max workflow call depth {} reached.".format(workflow_call_max_depth)) # init workflow run state + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) self.graph_engine = GraphEngine( tenant_id=tenant_id, app_id=app_id, @@ -79,7 +82,7 @@ class WorkflowEntry: call_depth=call_depth, graph=graph, graph_config=graph_config, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS, max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME, thread_pool_id=thread_pool_id, @@ -119,7 +122,9 @@ class WorkflowEntry: workflow: Workflow, node_id: str, user_id: str, - user_inputs: dict, + user_inputs: Mapping[str, Any], + variable_pool: VariablePool, + variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER, ) -> tuple[BaseNode, Generator[NodeEvent | InNodeEvent, None, None]]: """ Single step run workflow node @@ -129,29 +134,14 @@ class WorkflowEntry: :param user_inputs: user inputs :return: """ - # fetch node info from workflow graph - workflow_graph = workflow.graph_dict - if not workflow_graph: - raise ValueError("workflow graph not found") - - nodes = workflow_graph.get("nodes") - if not nodes: - raise ValueError("nodes not found in workflow graph") - - # fetch node config from node id - try: - node_config = next(filter(lambda node: node["id"] == node_id, nodes)) - except StopIteration: - raise ValueError("node id not found in workflow graph") + node_config = workflow.get_node_config_by_id(node_id) + node_config_data = node_config.get("data", {}) # Get node class - node_type = NodeType(node_config.get("data", {}).get("type")) - node_version = node_config.get("data", {}).get("version", "1") + node_type = NodeType(node_config_data.get("type")) + node_version = node_config_data.get("version", "1") node_cls = NODE_TYPE_CLASSES_MAPPING[node_type][node_version] - # init variable pool - variable_pool = VariablePool(environment_variables=workflow.environment_variables) - # init graph graph = Graph.init(graph_config=workflow.graph_dict) @@ -182,16 +172,33 @@ class WorkflowEntry: except NotImplementedError: variable_mapping = {} + # Loading missing variable from draft var here, and set it into + # variable_pool. + load_into_variable_pool( + variable_loader=variable_loader, + variable_pool=variable_pool, + variable_mapping=variable_mapping, + user_inputs=user_inputs, + ) + cls.mapping_user_inputs_to_variable_pool( variable_mapping=variable_mapping, user_inputs=user_inputs, variable_pool=variable_pool, tenant_id=workflow.tenant_id, ) + try: # run node generator = node_instance.run() except Exception as e: + logger.exception( + "error while running node_instance, workflow_id=%s, node_id=%s, type=%s, version=%s", + workflow.id, + node_instance.id, + node_instance.node_type, + node_instance.version(), + ) raise WorkflowNodeRunFailedError(node_instance=node_instance, error=str(e)) return node_instance, generator @@ -248,7 +255,7 @@ class WorkflowEntry: # init variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, environment_variables=[], ) @@ -294,10 +301,20 @@ class WorkflowEntry: return node_instance, generator except Exception as e: + logger.exception( + "error while running node_instance, node_id=%s, type=%s, version=%s", + node_instance.id, + node_instance.node_type, + node_instance.version(), + ) raise WorkflowNodeRunFailedError(node_instance=node_instance, error=str(e)) @staticmethod def handle_special_values(value: Optional[Mapping[str, Any]]) -> Mapping[str, Any] | None: + # NOTE(QuantumGhost): Avoid using this function in new code. + # Keep values structured as long as possible and only convert to dict + # immediately before serialization (e.g., JSON serialization) to maintain + # data integrity and type information. result = WorkflowEntry._handle_special_values(value) return result if isinstance(result, Mapping) or result is None else dict(result) @@ -324,10 +341,17 @@ class WorkflowEntry: cls, *, variable_mapping: Mapping[str, Sequence[str]], - user_inputs: dict, + user_inputs: Mapping[str, Any], variable_pool: VariablePool, tenant_id: str, ) -> None: + # NOTE(QuantumGhost): This logic should remain synchronized with + # the implementation of `load_into_variable_pool`, specifically the logic about + # variable existence checking. + + # WARNING(QuantumGhost): The semantics of this method are not clearly defined, + # and multiple parts of the codebase depend on its current behavior. + # Modify with caution. for node_variable, variable_selector in variable_mapping.items(): # fetch node id and variable key from node_variable node_variable_list = node_variable.split(".") diff --git a/api/core/workflow/workflow_type_encoder.py b/api/core/workflow/workflow_type_encoder.py new file mode 100644 index 0000000000..0123fdac18 --- /dev/null +++ b/api/core/workflow/workflow_type_encoder.py @@ -0,0 +1,49 @@ +import json +from collections.abc import Mapping +from typing import Any + +from pydantic import BaseModel + +from core.file.models import File +from core.variables import Segment + + +class WorkflowRuntimeTypeEncoder(json.JSONEncoder): + def default(self, o: Any): + if isinstance(o, Segment): + return o.value + elif isinstance(o, File): + return o.to_dict() + elif isinstance(o, BaseModel): + return o.model_dump(mode="json") + else: + return super().default(o) + + +class WorkflowRuntimeTypeConverter: + def to_json_encodable(self, value: Mapping[str, Any] | None) -> Mapping[str, Any] | None: + result = self._to_json_encodable_recursive(value) + return result if isinstance(result, Mapping) or result is None else dict(result) + + def _to_json_encodable_recursive(self, value: Any) -> Any: + if value is None: + return value + if isinstance(value, (bool, int, str, float)): + return value + if isinstance(value, Segment): + return self._to_json_encodable_recursive(value.value) + if isinstance(value, File): + return value.to_dict() + if isinstance(value, BaseModel): + return value.model_dump(mode="json") + if isinstance(value, dict): + res = {} + for k, v in value.items(): + res[k] = self._to_json_encodable_recursive(v) + return res + if isinstance(value, list): + res_list = [] + for item in value: + res_list.append(self._to_json_encodable_recursive(item)) + return res_list + return value diff --git a/api/events/event_handlers/__init__.py b/api/events/event_handlers/__init__.py index 1d6ad35333..ebc55d5ef8 100644 --- a/api/events/event_handlers/__init__.py +++ b/api/events/event_handlers/__init__.py @@ -3,8 +3,10 @@ from .clean_when_document_deleted import handle from .create_document_index import handle from .create_installed_app_when_app_created import handle from .create_site_record_when_app_created import handle -from .deduct_quota_when_message_created import handle from .delete_tool_parameters_cache_when_sync_draft_workflow import handle from .update_app_dataset_join_when_app_model_config_updated import handle from .update_app_dataset_join_when_app_published_workflow_updated import handle -from .update_provider_last_used_at_when_message_created import handle + +# Consolidated handler replaces both deduct_quota_when_message_created and +# update_provider_last_used_at_when_message_created +from .update_provider_when_message_created import handle diff --git a/api/events/event_handlers/deduct_quota_when_message_created.py b/api/events/event_handlers/deduct_quota_when_message_created.py deleted file mode 100644 index b8e7019446..0000000000 --- a/api/events/event_handlers/deduct_quota_when_message_created.py +++ /dev/null @@ -1,65 +0,0 @@ -from datetime import UTC, datetime - -from configs import dify_config -from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ChatAppGenerateEntity -from core.entities.provider_entities import QuotaUnit -from core.plugin.entities.plugin import ModelProviderID -from events.message_event import message_was_created -from extensions.ext_database import db -from models.provider import Provider, ProviderType - - -@message_was_created.connect -def handle(sender, **kwargs): - message = sender - application_generate_entity = kwargs.get("application_generate_entity") - - if not isinstance(application_generate_entity, ChatAppGenerateEntity | AgentChatAppGenerateEntity): - return - - model_config = application_generate_entity.model_conf - provider_model_bundle = model_config.provider_model_bundle - provider_configuration = provider_model_bundle.configuration - - if provider_configuration.using_provider_type != ProviderType.SYSTEM: - return - - system_configuration = provider_configuration.system_configuration - - if not system_configuration.current_quota_type: - return - - quota_unit = None - for quota_configuration in system_configuration.quota_configurations: - if quota_configuration.quota_type == system_configuration.current_quota_type: - quota_unit = quota_configuration.quota_unit - - if quota_configuration.quota_limit == -1: - return - - break - - used_quota = None - if quota_unit: - if quota_unit == QuotaUnit.TOKENS: - used_quota = message.message_tokens + message.answer_tokens - elif quota_unit == QuotaUnit.CREDITS: - used_quota = dify_config.get_model_credits(model_config.model) - else: - used_quota = 1 - - if used_quota is not None and system_configuration.current_quota_type is not None: - db.session.query(Provider).filter( - Provider.tenant_id == application_generate_entity.app_config.tenant_id, - # TODO: Use provider name with prefix after the data migration. - Provider.provider_name == ModelProviderID(model_config.provider).provider_name, - Provider.provider_type == ProviderType.SYSTEM.value, - Provider.quota_type == system_configuration.current_quota_type.value, - Provider.quota_limit > Provider.quota_used, - ).update( - { - "quota_used": Provider.quota_used + used_quota, - "last_used": datetime.now(tz=UTC).replace(tzinfo=None), - } - ) - db.session.commit() diff --git a/api/events/event_handlers/update_provider_last_used_at_when_message_created.py b/api/events/event_handlers/update_provider_last_used_at_when_message_created.py deleted file mode 100644 index 59412cf87c..0000000000 --- a/api/events/event_handlers/update_provider_last_used_at_when_message_created.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import UTC, datetime - -from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ChatAppGenerateEntity -from events.message_event import message_was_created -from extensions.ext_database import db -from models.provider import Provider - - -@message_was_created.connect -def handle(sender, **kwargs): - application_generate_entity = kwargs.get("application_generate_entity") - - if not isinstance(application_generate_entity, ChatAppGenerateEntity | AgentChatAppGenerateEntity): - return - - db.session.query(Provider).filter( - Provider.tenant_id == application_generate_entity.app_config.tenant_id, - Provider.provider_name == application_generate_entity.model_conf.provider, - ).update({"last_used": datetime.now(UTC).replace(tzinfo=None)}) - db.session.commit() diff --git a/api/events/event_handlers/update_provider_when_message_created.py b/api/events/event_handlers/update_provider_when_message_created.py new file mode 100644 index 0000000000..d3943f2eda --- /dev/null +++ b/api/events/event_handlers/update_provider_when_message_created.py @@ -0,0 +1,234 @@ +import logging +import time as time_module +from datetime import datetime +from typing import Any, Optional + +from pydantic import BaseModel +from sqlalchemy import update +from sqlalchemy.orm import Session + +from configs import dify_config +from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, ChatAppGenerateEntity +from core.entities.provider_entities import QuotaUnit, SystemConfiguration +from core.plugin.entities.plugin import ModelProviderID +from events.message_event import message_was_created +from extensions.ext_database import db +from libs import datetime_utils +from models.model import Message +from models.provider import Provider, ProviderType + +logger = logging.getLogger(__name__) + + +class _ProviderUpdateFilters(BaseModel): + """Filters for identifying Provider records to update.""" + + tenant_id: str + provider_name: str + provider_type: Optional[str] = None + quota_type: Optional[str] = None + + +class _ProviderUpdateAdditionalFilters(BaseModel): + """Additional filters for Provider updates.""" + + quota_limit_check: bool = False + + +class _ProviderUpdateValues(BaseModel): + """Values to update in Provider records.""" + + last_used: Optional[datetime] = None + quota_used: Optional[Any] = None # Can be Provider.quota_used + int expression + + +class _ProviderUpdateOperation(BaseModel): + """A single Provider update operation.""" + + filters: _ProviderUpdateFilters + values: _ProviderUpdateValues + additional_filters: _ProviderUpdateAdditionalFilters = _ProviderUpdateAdditionalFilters() + description: str = "unknown" + + +@message_was_created.connect +def handle(sender: Message, **kwargs): + """ + Consolidated handler for Provider updates when a message is created. + + This handler replaces both: + - update_provider_last_used_at_when_message_created + - deduct_quota_when_message_created + + By performing all Provider updates in a single transaction, we ensure + consistency and efficiency when updating Provider records. + """ + message = sender + application_generate_entity = kwargs.get("application_generate_entity") + + if not isinstance(application_generate_entity, ChatAppGenerateEntity | AgentChatAppGenerateEntity): + return + + tenant_id = application_generate_entity.app_config.tenant_id + provider_name = application_generate_entity.model_conf.provider + current_time = datetime_utils.naive_utc_now() + + # Prepare updates for both scenarios + updates_to_perform: list[_ProviderUpdateOperation] = [] + + # 1. Always update last_used for the provider + basic_update = _ProviderUpdateOperation( + filters=_ProviderUpdateFilters( + tenant_id=tenant_id, + provider_name=provider_name, + ), + values=_ProviderUpdateValues(last_used=current_time), + description="basic_last_used_update", + ) + updates_to_perform.append(basic_update) + + # 2. Check if we need to deduct quota (system provider only) + model_config = application_generate_entity.model_conf + provider_model_bundle = model_config.provider_model_bundle + provider_configuration = provider_model_bundle.configuration + + if ( + provider_configuration.using_provider_type == ProviderType.SYSTEM + and provider_configuration.system_configuration + and provider_configuration.system_configuration.current_quota_type is not None + ): + system_configuration = provider_configuration.system_configuration + + # Calculate quota usage + used_quota = _calculate_quota_usage( + message=message, + system_configuration=system_configuration, + model_name=model_config.model, + ) + + if used_quota is not None: + quota_update = _ProviderUpdateOperation( + filters=_ProviderUpdateFilters( + tenant_id=tenant_id, + provider_name=ModelProviderID(model_config.provider).provider_name, + provider_type=ProviderType.SYSTEM.value, + quota_type=provider_configuration.system_configuration.current_quota_type.value, + ), + values=_ProviderUpdateValues(quota_used=Provider.quota_used + used_quota, last_used=current_time), + additional_filters=_ProviderUpdateAdditionalFilters( + quota_limit_check=True # Provider.quota_limit > Provider.quota_used + ), + description="quota_deduction_update", + ) + updates_to_perform.append(quota_update) + + # Execute all updates + start_time = time_module.perf_counter() + try: + _execute_provider_updates(updates_to_perform) + + # Log successful completion with timing + duration = time_module.perf_counter() - start_time + + logger.info( + f"Provider updates completed successfully. " + f"Updates: {len(updates_to_perform)}, Duration: {duration:.3f}s, " + f"Tenant: {tenant_id}, Provider: {provider_name}" + ) + + except Exception as e: + # Log failure with timing and context + duration = time_module.perf_counter() - start_time + + logger.exception( + f"Provider updates failed after {duration:.3f}s. " + f"Updates: {len(updates_to_perform)}, Tenant: {tenant_id}, " + f"Provider: {provider_name}" + ) + raise + + +def _calculate_quota_usage( + *, message: Message, system_configuration: SystemConfiguration, model_name: str +) -> Optional[int]: + """Calculate quota usage based on message tokens and quota type.""" + quota_unit = None + for quota_configuration in system_configuration.quota_configurations: + if quota_configuration.quota_type == system_configuration.current_quota_type: + quota_unit = quota_configuration.quota_unit + if quota_configuration.quota_limit == -1: + return None + break + if quota_unit is None: + return None + + try: + if quota_unit == QuotaUnit.TOKENS: + tokens = message.message_tokens + message.answer_tokens + return tokens + if quota_unit == QuotaUnit.CREDITS: + tokens = dify_config.get_model_credits(model_name) + return tokens + elif quota_unit == QuotaUnit.TIMES: + return 1 + return None + except Exception as e: + logger.exception("Failed to calculate quota usage") + return None + + +def _execute_provider_updates(updates_to_perform: list[_ProviderUpdateOperation]): + """Execute all Provider updates in a single transaction.""" + if not updates_to_perform: + return + + # Use SQLAlchemy's context manager for transaction management + # This automatically handles commit/rollback + with Session(db.engine) as session: + # Use a single transaction for all updates + for update_operation in updates_to_perform: + filters = update_operation.filters + values = update_operation.values + additional_filters = update_operation.additional_filters + description = update_operation.description + + # Build the where conditions + where_conditions = [ + Provider.tenant_id == filters.tenant_id, + Provider.provider_name == filters.provider_name, + ] + + # Add additional filters if specified + if filters.provider_type is not None: + where_conditions.append(Provider.provider_type == filters.provider_type) + if filters.quota_type is not None: + where_conditions.append(Provider.quota_type == filters.quota_type) + if additional_filters.quota_limit_check: + where_conditions.append(Provider.quota_limit > Provider.quota_used) + + # Prepare values dict for SQLAlchemy update + update_values = {} + if values.last_used is not None: + update_values["last_used"] = values.last_used + if values.quota_used is not None: + update_values["quota_used"] = values.quota_used + + # Build and execute the update statement + stmt = update(Provider).where(*where_conditions).values(**update_values) + result = session.execute(stmt) + rows_affected = result.rowcount + + logger.debug( + f"Provider update ({description}): {rows_affected} rows affected. " + f"Filters: {filters.model_dump()}, Values: {update_values}" + ) + + # If no rows were affected for quota updates, log a warning + if rows_affected == 0 and description == "quota_deduction_update": + logger.warning( + f"No Provider rows updated for quota deduction. " + f"This may indicate quota limit exceeded or provider not found. " + f"Filters: {filters.model_dump()}" + ) + + logger.debug(f"Successfully processed {len(updates_to_perform)} Provider updates") diff --git a/api/extensions/ext_app_metrics.py b/api/extensions/ext_app_metrics.py index b7d412d68d..56a69a1862 100644 --- a/api/extensions/ext_app_metrics.py +++ b/api/extensions/ext_app_metrics.py @@ -12,14 +12,14 @@ def init_app(app: DifyApp): @app.after_request def after_request(response): """Add Version headers to the response.""" - response.headers.add("X-Version", dify_config.CURRENT_VERSION) + response.headers.add("X-Version", dify_config.project.version) response.headers.add("X-Env", dify_config.DEPLOY_ENV) return response @app.route("/health") def health(): return Response( - json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}), + json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.project.version}), status=200, content_type="application/json", ) diff --git a/api/extensions/ext_blueprints.py b/api/extensions/ext_blueprints.py index 316be12f5c..a4d013ffc0 100644 --- a/api/extensions/ext_blueprints.py +++ b/api/extensions/ext_blueprints.py @@ -10,6 +10,7 @@ def init_app(app: DifyApp): from controllers.console import bp as console_app_bp from controllers.files import bp as files_bp from controllers.inner_api import bp as inner_api_bp + from controllers.mcp import bp as mcp_bp from controllers.service_api import bp as service_api_bp from controllers.web import bp as web_bp @@ -46,3 +47,4 @@ def init_app(app: DifyApp): app.register_blueprint(files_bp) app.register_blueprint(inner_api_bp) + app.register_blueprint(mcp_bp) diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index a837552007..6279b1ad36 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -21,6 +21,7 @@ def init_app(app: DifyApp) -> Celery: "master_name": dify_config.CELERY_SENTINEL_MASTER_NAME, "sentinel_kwargs": { "socket_timeout": dify_config.CELERY_SENTINEL_SOCKET_TIMEOUT, + "password": dify_config.CELERY_SENTINEL_PASSWORD, }, } diff --git a/api/extensions/ext_login.py b/api/extensions/ext_login.py index 06f42494ec..11d1856ac4 100644 --- a/api/extensions/ext_login.py +++ b/api/extensions/ext_login.py @@ -10,7 +10,7 @@ from dify_app import DifyApp from extensions.ext_database import db from libs.passport import PassportService from models.account import Account, Tenant, TenantAccountJoin -from models.model import EndUser +from models.model import AppMCPServer, EndUser from services.account_service import AccountService login_manager = flask_login.LoginManager() @@ -57,6 +57,9 @@ def load_user_from_request(request_from_flask_login): raise Unauthorized("Invalid Authorization token.") decoded = PassportService().verify(auth_token) user_id = decoded.get("user_id") + source = decoded.get("token_source") + if source: + raise Unauthorized("Invalid Authorization token.") if not user_id: raise Unauthorized("Invalid Authorization token.") @@ -71,6 +74,21 @@ def load_user_from_request(request_from_flask_login): if not end_user: raise NotFound("End user not found.") return end_user + elif request.blueprint == "mcp": + server_code = request.view_args.get("server_code") if request.view_args else None + if not server_code: + raise Unauthorized("Invalid Authorization token.") + app_mcp_server = db.session.query(AppMCPServer).filter(AppMCPServer.server_code == server_code).first() + if not app_mcp_server: + raise NotFound("App MCP server not found.") + end_user = ( + db.session.query(EndUser) + .filter(EndUser.external_user_id == app_mcp_server.id, EndUser.type == "mcp") + .first() + ) + if not end_user: + raise NotFound("End user not found.") + return end_user @user_logged_in.connect diff --git a/api/extensions/ext_mail.py b/api/extensions/ext_mail.py index 84bc12eca0..df5d8a9c11 100644 --- a/api/extensions/ext_mail.py +++ b/api/extensions/ext_mail.py @@ -54,6 +54,15 @@ class Mail: use_tls=dify_config.SMTP_USE_TLS, opportunistic_tls=dify_config.SMTP_OPPORTUNISTIC_TLS, ) + case "sendgrid": + from libs.sendgrid import SendGridClient + + if not dify_config.SENDGRID_API_KEY: + raise ValueError("SENDGRID_API_KEY is required for SendGrid mail type") + + self._client = SendGridClient( + sendgrid_api_key=dify_config.SENDGRID_API_KEY, _from=dify_config.MAIL_DEFAULT_SEND_FROM or "" + ) case _: raise ValueError("Unsupported mail type {}".format(mail_type)) diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py index 6dcfa7bec6..b62b0b60d6 100644 --- a/api/extensions/ext_otel.py +++ b/api/extensions/ext_otel.py @@ -12,6 +12,7 @@ from flask_login import user_loaded_from_request, user_logged_in # type: ignore from configs import dify_config from dify_app import DifyApp +from libs.helper import extract_tenant_id from models import Account, EndUser @@ -24,11 +25,8 @@ def on_user_loaded(_sender, user: Union["Account", "EndUser"]): if user: try: current_span = get_current_span() - if isinstance(user, Account) and user.current_tenant_id: - tenant_id = user.current_tenant_id - elif isinstance(user, EndUser): - tenant_id = user.tenant_id - else: + tenant_id = extract_tenant_id(user) + if not tenant_id: return if current_span: current_span.set_attribute("service.tenant.id", tenant_id) @@ -49,7 +47,7 @@ def init_app(app: DifyApp): logging.getLogger().addHandler(exception_handler) def init_flask_instrumentor(app: DifyApp): - meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION) + meter = get_meter("http_metrics", version=dify_config.project.version) _http_response_counter = meter.create_counter( "http.server.response.count", description="Total number of HTTP responses by status code, method and target", @@ -163,7 +161,7 @@ def init_app(app: DifyApp): resource = Resource( attributes={ ResourceAttributes.SERVICE_NAME: dify_config.APPLICATION_NAME, - ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}", + ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", ResourceAttributes.PROCESS_PID: os.getpid(), ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}", ResourceAttributes.HOST_NAME: socket.gethostname(), diff --git a/api/extensions/ext_redis.py b/api/extensions/ext_redis.py index c283b1b7ca..be2f6115f7 100644 --- a/api/extensions/ext_redis.py +++ b/api/extensions/ext_redis.py @@ -1,6 +1,10 @@ +import functools +import logging +from collections.abc import Callable from typing import Any, Union import redis +from redis import RedisError from redis.cache import CacheConfig from redis.cluster import ClusterNode, RedisCluster from redis.connection import Connection, SSLConnection @@ -9,6 +13,8 @@ from redis.sentinel import Sentinel from configs import dify_config from dify_app import DifyApp +logger = logging.getLogger(__name__) + class RedisClientWrapper: """ @@ -115,3 +121,25 @@ def init_app(app: DifyApp): redis_client.initialize(redis.Redis(connection_pool=pool)) app.extensions["redis"] = redis_client + + +def redis_fallback(default_return: Any = None): + """ + decorator to handle Redis operation exceptions and return a default value when Redis is unavailable. + + Args: + default_return: The value to return when a Redis operation fails. Defaults to None. + """ + + def decorator(func: Callable): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except RedisError as e: + logger.warning(f"Redis operation failed in {func.__name__}: {str(e)}", exc_info=True) + return default_return + + return wrapper + + return decorator diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index 3a74aace6a..82aed0d98d 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -35,6 +35,6 @@ def init_app(app: DifyApp): traces_sample_rate=dify_config.SENTRY_TRACES_SAMPLE_RATE, profiles_sample_rate=dify_config.SENTRY_PROFILES_SAMPLE_RATE, environment=dify_config.DEPLOY_ENV, - release=f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}", + release=f"dify-{dify_config.project.version}-{dify_config.COMMIT_SHA}", before_send=before_send, ) diff --git a/api/factories/agent_factory.py b/api/factories/agent_factory.py index 4b12afb528..2570bc22f1 100644 --- a/api/factories/agent_factory.py +++ b/api/factories/agent_factory.py @@ -10,6 +10,6 @@ def get_plugin_agent_strategy( agent_provider = manager.fetch_agent_strategy_provider(tenant_id, agent_strategy_provider_name) for agent_strategy in agent_provider.declaration.strategies: if agent_strategy.identity.name == agent_strategy_name: - return PluginAgentStrategy(tenant_id, agent_strategy) + return PluginAgentStrategy(tenant_id, agent_strategy, agent_provider.meta.version) raise ValueError(f"Agent strategy {agent_strategy_name} not found") diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 52f119936f..25d1390492 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -5,6 +5,7 @@ from typing import Any, cast import httpx from sqlalchemy import select +from sqlalchemy.orm import Session from constants import AUDIO_EXTENSIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, VIDEO_EXTENSIONS from core.file import File, FileBelongsTo, FileTransferMethod, FileType, FileUploadConfig, helpers @@ -91,6 +92,8 @@ def build_from_mappings( tenant_id: str, strict_type_validation: bool = False, ) -> Sequence[File]: + # TODO(QuantumGhost): Performance concern - each mapping triggers a separate database query. + # Implement batch processing to reduce database load when handling multiple files. files = [ build_from_mapping( mapping=mapping, @@ -377,3 +380,75 @@ def _get_file_type_by_mimetype(mime_type: str) -> FileType | None: def get_file_type_by_mime_type(mime_type: str) -> FileType: return _get_file_type_by_mimetype(mime_type) or FileType.CUSTOM + + +class StorageKeyLoader: + """FileKeyLoader load the storage key from database for a list of files. + This loader is batched, the database query count is constant regardless of the input size. + """ + + def __init__(self, session: Session, tenant_id: str) -> None: + self._session = session + self._tenant_id = tenant_id + + def _load_upload_files(self, upload_file_ids: Sequence[uuid.UUID]) -> Mapping[uuid.UUID, UploadFile]: + stmt = select(UploadFile).where( + UploadFile.id.in_(upload_file_ids), + UploadFile.tenant_id == self._tenant_id, + ) + + return {uuid.UUID(i.id): i for i in self._session.scalars(stmt)} + + def _load_tool_files(self, tool_file_ids: Sequence[uuid.UUID]) -> Mapping[uuid.UUID, ToolFile]: + stmt = select(ToolFile).where( + ToolFile.id.in_(tool_file_ids), + ToolFile.tenant_id == self._tenant_id, + ) + return {uuid.UUID(i.id): i for i in self._session.scalars(stmt)} + + def load_storage_keys(self, files: Sequence[File]): + """Loads storage keys for a sequence of files by retrieving the corresponding + `UploadFile` or `ToolFile` records from the database based on their transfer method. + + This method doesn't modify the input sequence structure but updates the `_storage_key` + property of each file object by extracting the relevant key from its database record. + + Performance note: This is a batched operation where database query count remains constant + regardless of input size. However, for optimal performance, input sequences should contain + fewer than 1000 files. For larger collections, split into smaller batches and process each + batch separately. + """ + + upload_file_ids: list[uuid.UUID] = [] + tool_file_ids: list[uuid.UUID] = [] + for file in files: + related_model_id = file.related_id + if file.related_id is None: + raise ValueError("file id should not be None.") + if file.tenant_id != self._tenant_id: + err_msg = ( + f"invalid file, expected tenant_id={self._tenant_id}, " + f"got tenant_id={file.tenant_id}, file_id={file.id}, related_model_id={related_model_id}" + ) + raise ValueError(err_msg) + model_id = uuid.UUID(related_model_id) + + if file.transfer_method in (FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL): + upload_file_ids.append(model_id) + elif file.transfer_method == FileTransferMethod.TOOL_FILE: + tool_file_ids.append(model_id) + + tool_files = self._load_tool_files(tool_file_ids) + upload_files = self._load_upload_files(upload_file_ids) + for file in files: + model_id = uuid.UUID(file.related_id) + if file.transfer_method in (FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL): + upload_file_row = upload_files.get(model_id) + if upload_file_row is None: + raise ValueError(f"Upload file not found for id: {model_id}") + file._storage_key = upload_file_row.key + elif file.transfer_method == FileTransferMethod.TOOL_FILE: + tool_file_row = tool_files.get(model_id) + if tool_file_row is None: + raise ValueError(f"Tool file not found for id: {model_id}") + file._storage_key = tool_file_row.file_key diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index fa8a90e79f..39ebd009d5 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -39,11 +39,11 @@ from core.variables.variables import ( from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID -class InvalidSelectorError(ValueError): +class UnsupportedSegmentTypeError(Exception): pass -class UnsupportedSegmentTypeError(Exception): +class TypeMismatchError(Exception): pass @@ -91,9 +91,13 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen result = StringVariable.model_validate(mapping) case SegmentType.SECRET: result = SecretVariable.model_validate(mapping) - case SegmentType.NUMBER if isinstance(value, int): + case SegmentType.NUMBER | SegmentType.INTEGER if isinstance(value, int): + mapping = dict(mapping) + mapping["value_type"] = SegmentType.INTEGER result = IntegerVariable.model_validate(mapping) - case SegmentType.NUMBER if isinstance(value, float): + case SegmentType.NUMBER | SegmentType.FLOAT if isinstance(value, float): + mapping = dict(mapping) + mapping["value_type"] = SegmentType.FLOAT result = FloatVariable.model_validate(mapping) case SegmentType.NUMBER if not isinstance(value, float | int): raise VariableError(f"invalid number value {value}") @@ -114,7 +118,13 @@ def _build_variable_from_mapping(*, mapping: Mapping[str, Any], selector: Sequen return cast(Variable, result) +def infer_segment_type_from_value(value: Any, /) -> SegmentType: + return build_segment(value).value_type + + def build_segment(value: Any, /) -> Segment: + # NOTE: If you have runtime type information available, consider using the `build_segment_with_type` + # below if value is None: return NoneSegment() if isinstance(value, str): @@ -130,12 +140,17 @@ def build_segment(value: Any, /) -> Segment: if isinstance(value, list): items = [build_segment(item) for item in value] types = {item.value_type for item in items} - if len(types) != 1 or all(isinstance(item, ArraySegment) for item in items): + if all(isinstance(item, ArraySegment) for item in items): return ArrayAnySegment(value=value) + elif len(types) != 1: + if types.issubset({SegmentType.NUMBER, SegmentType.INTEGER, SegmentType.FLOAT}): + return ArrayNumberSegment(value=value) + return ArrayAnySegment(value=value) + match types.pop(): case SegmentType.STRING: return ArrayStringSegment(value=value) - case SegmentType.NUMBER: + case SegmentType.NUMBER | SegmentType.INTEGER | SegmentType.FLOAT: return ArrayNumberSegment(value=value) case SegmentType.OBJECT: return ArrayObjectSegment(value=value) @@ -144,10 +159,100 @@ def build_segment(value: Any, /) -> Segment: case SegmentType.NONE: return ArrayAnySegment(value=value) case _: + # This should be unreachable. raise ValueError(f"not supported value {value}") raise ValueError(f"not supported value {value}") +_segment_factory: Mapping[SegmentType, type[Segment]] = { + SegmentType.NONE: NoneSegment, + SegmentType.STRING: StringSegment, + SegmentType.INTEGER: IntegerSegment, + SegmentType.FLOAT: FloatSegment, + SegmentType.FILE: FileSegment, + SegmentType.OBJECT: ObjectSegment, + # Array types + SegmentType.ARRAY_ANY: ArrayAnySegment, + SegmentType.ARRAY_STRING: ArrayStringSegment, + SegmentType.ARRAY_NUMBER: ArrayNumberSegment, + SegmentType.ARRAY_OBJECT: ArrayObjectSegment, + SegmentType.ARRAY_FILE: ArrayFileSegment, +} + + +def build_segment_with_type(segment_type: SegmentType, value: Any) -> Segment: + """ + Build a segment with explicit type checking. + + This function creates a segment from a value while enforcing type compatibility + with the specified segment_type. It provides stricter type validation compared + to the standard build_segment function. + + Args: + segment_type: The expected SegmentType for the resulting segment + value: The value to be converted into a segment + + Returns: + Segment: A segment instance of the appropriate type + + Raises: + TypeMismatchError: If the value type doesn't match the expected segment_type + + Special Cases: + - For empty list [] values, if segment_type is array[*], returns the corresponding array type + - Type validation is performed before segment creation + + Examples: + >>> build_segment_with_type(SegmentType.STRING, "hello") + StringSegment(value="hello") + + >>> build_segment_with_type(SegmentType.ARRAY_STRING, []) + ArrayStringSegment(value=[]) + + >>> build_segment_with_type(SegmentType.STRING, 123) + # Raises TypeMismatchError + """ + # Handle None values + if value is None: + if segment_type == SegmentType.NONE: + return NoneSegment() + else: + raise TypeMismatchError(f"Type mismatch: expected {segment_type}, but got None") + + # Handle empty list special case for array types + if isinstance(value, list) and len(value) == 0: + if segment_type == SegmentType.ARRAY_ANY: + return ArrayAnySegment(value=value) + elif segment_type == SegmentType.ARRAY_STRING: + return ArrayStringSegment(value=value) + elif segment_type == SegmentType.ARRAY_NUMBER: + return ArrayNumberSegment(value=value) + elif segment_type == SegmentType.ARRAY_OBJECT: + return ArrayObjectSegment(value=value) + elif segment_type == SegmentType.ARRAY_FILE: + return ArrayFileSegment(value=value) + else: + raise TypeMismatchError(f"Type mismatch: expected {segment_type}, but got empty list") + + inferred_type = SegmentType.infer_segment_type(value) + # Type compatibility checking + if inferred_type is None: + raise TypeMismatchError( + f"Type mismatch: expected {segment_type}, but got python object, type={type(value)}, value={value}" + ) + if inferred_type == segment_type: + segment_class = _segment_factory[segment_type] + return segment_class(value_type=segment_type, value=value) + elif segment_type == SegmentType.NUMBER and inferred_type in ( + SegmentType.INTEGER, + SegmentType.FLOAT, + ): + segment_class = _segment_factory[inferred_type] + return segment_class(value_type=inferred_type, value=value) + else: + raise TypeMismatchError(f"Type mismatch: expected {segment_type}, but got {inferred_type}, value={value}") + + def segment_to_variable( *, segment: Segment, @@ -173,6 +278,6 @@ def segment_to_variable( name=name, description=description, value=segment.value, - selector=selector, + selector=list(selector), ), ) diff --git a/api/fields/_value_type_serializer.py b/api/fields/_value_type_serializer.py new file mode 100644 index 0000000000..8288bd54a3 --- /dev/null +++ b/api/fields/_value_type_serializer.py @@ -0,0 +1,15 @@ +from typing import TypedDict + +from core.variables.segments import Segment +from core.variables.types import SegmentType + + +class _VarTypedDict(TypedDict, total=False): + value_type: SegmentType + + +def serialize_value_type(v: _VarTypedDict | Segment) -> str: + if isinstance(v, Segment): + return v.value_type.exposed_type().value + else: + return v["value_type"].exposed_type().value diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 500ca47c7e..73c224542a 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -1,8 +1,21 @@ +import json + from flask_restful import fields from fields.workflow_fields import workflow_partial_fields from libs.helper import AppIconUrlField, TimestampField + +class JsonStringField(fields.Raw): + def format(self, value): + if isinstance(value, str): + try: + return json.loads(value) + except (json.JSONDecodeError, TypeError): + return value + return value + + app_detail_kernel_fields = { "id": fields.String, "name": fields.String, @@ -218,3 +231,14 @@ app_import_fields = { app_import_check_dependencies_fields = { "leaked_dependencies": fields.List(fields.Nested(leaked_dependency_fields)), } + +app_server_fields = { + "id": fields.String, + "name": fields.String, + "server_code": fields.String, + "description": fields.String, + "status": fields.String, + "parameters": JsonStringField, + "created_at": TimestampField, + "updated_at": TimestampField, +} diff --git a/api/fields/conversation_variable_fields.py b/api/fields/conversation_variable_fields.py index 71785e7d67..c5a0c9a49d 100644 --- a/api/fields/conversation_variable_fields.py +++ b/api/fields/conversation_variable_fields.py @@ -2,10 +2,12 @@ from flask_restful import fields from libs.helper import TimestampField +from ._value_type_serializer import serialize_value_type + conversation_variable_fields = { "id": fields.String, "name": fields.String, - "value_type": fields.String(attribute="value_type.value"), + "value_type": fields.String(attribute=serialize_value_type), "value": fields.String, "description": fields.String, "created_at": TimestampField, diff --git a/api/fields/workflow_fields.py b/api/fields/workflow_fields.py index 9f1bef3b36..930e59cc1c 100644 --- a/api/fields/workflow_fields.py +++ b/api/fields/workflow_fields.py @@ -5,6 +5,8 @@ from core.variables import SecretVariable, SegmentType, Variable from fields.member_fields import simple_account_fields from libs.helper import TimestampField +from ._value_type_serializer import serialize_value_type + ENVIRONMENT_VARIABLE_SUPPORTED_TYPES = (SegmentType.STRING, SegmentType.NUMBER, SegmentType.SECRET) @@ -17,16 +19,23 @@ class EnvironmentVariableField(fields.Raw): "name": value.name, "value": encrypter.obfuscated_token(value.value), "value_type": value.value_type.value, + "description": value.description, } if isinstance(value, Variable): return { "id": value.id, "name": value.name, "value": value.value, - "value_type": value.value_type.value, + "value_type": value.value_type.exposed_type().value, + "description": value.description, } if isinstance(value, dict): - value_type = value.get("value_type") + value_type_str = value.get("value_type") + if not isinstance(value_type_str, str): + raise TypeError( + f"unexpected type for value_type field, value={value_type_str}, type={type(value_type_str)}" + ) + value_type = SegmentType(value_type_str).exposed_type() if value_type not in ENVIRONMENT_VARIABLE_SUPPORTED_TYPES: raise ValueError(f"Unsupported environment variable value type: {value_type}") return value @@ -35,7 +44,7 @@ class EnvironmentVariableField(fields.Raw): conversation_variable_fields = { "id": fields.String, "name": fields.String, - "value_type": fields.String(attribute="value_type.value"), + "value_type": fields.String(attribute=serialize_value_type), "value": fields.Raw, "description": fields.String, } diff --git a/api/fields/workflow_run_fields.py b/api/fields/workflow_run_fields.py index 74fdf8bd97..a106728e9c 100644 --- a/api/fields/workflow_run_fields.py +++ b/api/fields/workflow_run_fields.py @@ -19,7 +19,6 @@ workflow_run_for_log_fields = { workflow_run_for_list_fields = { "id": fields.String, - "sequence_number": fields.Integer, "version": fields.String, "status": fields.String, "elapsed_time": fields.Float, @@ -36,7 +35,6 @@ advanced_chat_workflow_run_for_list_fields = { "id": fields.String, "conversation_id": fields.String, "message_id": fields.String, - "sequence_number": fields.Integer, "version": fields.String, "status": fields.String, "elapsed_time": fields.Float, @@ -63,7 +61,6 @@ workflow_run_pagination_fields = { workflow_run_detail_fields = { "id": fields.String, - "sequence_number": fields.Integer, "version": fields.String, "graph": fields.Raw(attribute="graph_dict"), "inputs": fields.Raw(attribute="inputs_dict"), diff --git a/api/libs/datetime_utils.py b/api/libs/datetime_utils.py new file mode 100644 index 0000000000..e576a34629 --- /dev/null +++ b/api/libs/datetime_utils.py @@ -0,0 +1,22 @@ +import abc +import datetime +from typing import Protocol + + +class _NowFunction(Protocol): + @abc.abstractmethod + def __call__(self, tz: datetime.timezone | None) -> datetime.datetime: + pass + + +# _now_func is a callable with the _NowFunction signature. +# Its sole purpose is to abstract time retrieval, enabling +# developers to mock this behavior in tests and time-dependent scenarios. +_now_func: _NowFunction = datetime.datetime.now + + +def naive_utc_now() -> datetime.datetime: + """Return a naive datetime object (without timezone information) + representing current UTC time. + """ + return _now_func(datetime.UTC).replace(tzinfo=None) diff --git a/api/libs/file_utils.py b/api/libs/file_utils.py new file mode 100644 index 0000000000..982b2cc1ac --- /dev/null +++ b/api/libs/file_utils.py @@ -0,0 +1,30 @@ +from pathlib import Path + + +def search_file_upwards( + base_dir_path: Path, + target_file_name: str, + max_search_parent_depth: int, +) -> Path: + """ + Find a target file in the current directory or its parent directories up to a specified depth. + :param base_dir_path: Starting directory path to search from. + :param target_file_name: Name of the file to search for. + :param max_search_parent_depth: Maximum number of parent directories to search upwards. + :return: Path of the file if found, otherwise None. + """ + current_path = base_dir_path.resolve() + for _ in range(max_search_parent_depth): + candidate_path = current_path / target_file_name + if candidate_path.is_file(): + return candidate_path + parent_path = current_path.parent + if parent_path == current_path: # reached the root directory + break + else: + current_path = parent_path + + raise ValueError( + f"File '{target_file_name}' not found in the directory '{base_dir_path.resolve()}' or its parent directories" + f" in depth of {max_search_parent_depth}." + ) diff --git a/api/libs/flask_utils.py b/api/libs/flask_utils.py new file mode 100644 index 0000000000..4ea2779584 --- /dev/null +++ b/api/libs/flask_utils.py @@ -0,0 +1,65 @@ +import contextvars +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TypeVar + +from flask import Flask, g, has_request_context + +T = TypeVar("T") + + +@contextmanager +def preserve_flask_contexts( + flask_app: Flask, + context_vars: contextvars.Context, +) -> Iterator[None]: + """ + A context manager that handles: + 1. flask-login's UserProxy copy + 2. ContextVars copy + 3. flask_app.app_context() + + This context manager ensures that the Flask application context is properly set up, + the current user is preserved across context boundaries, and any provided context variables + are set within the new context. + + Note: + This manager aims to allow use current_user cross thread and app context, + but it's not the recommend use, it's better to pass user directly in parameters. + + Args: + flask_app: The Flask application instance + context_vars: contextvars.Context object containing context variables to be set in the new context + + Yields: + None + + Example: + ```python + with preserve_flask_contexts(flask_app, context_vars=context_vars): + # Code that needs Flask app context and context variables + # Current user will be preserved if available + ``` + """ + # Set context variables if provided + if context_vars: + for var, val in context_vars.items(): + var.set(val) + + # Save current user before entering new app context + saved_user = None + if has_request_context() and hasattr(g, "_login_user"): + saved_user = g._login_user + + # Enter Flask app context + with flask_app.app_context(): + try: + # Restore user in new app context if it was saved + if saved_user is not None: + g._login_user = saved_user + + # Yield control back to the caller + yield + finally: + # Any cleanup can be added here if needed + pass diff --git a/api/libs/helper.py b/api/libs/helper.py index 463ba3308b..48126461a3 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -1,8 +1,9 @@ import json import logging -import random import re +import secrets import string +import struct import subprocess import time import uuid @@ -14,6 +15,7 @@ from zoneinfo import available_timezones from flask import Response, stream_with_context from flask_restful import fields +from pydantic import BaseModel from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator @@ -23,6 +25,31 @@ from extensions.ext_redis import redis_client if TYPE_CHECKING: from models.account import Account + from models.model import EndUser + + +def extract_tenant_id(user: Union["Account", "EndUser"]) -> str | None: + """ + Extract tenant_id from Account or EndUser object. + + Args: + user: Account or EndUser object + + Returns: + tenant_id string if available, None otherwise + + Raises: + ValueError: If user is neither Account nor EndUser + """ + from models.account import Account + from models.model import EndUser + + if isinstance(user, Account): + return user.current_tenant_id + elif isinstance(user, EndUser): + return user.tenant_id + else: + raise ValueError(f"Invalid user type: {type(user)}. Expected Account or EndUser.") def run(script): @@ -176,14 +203,14 @@ def generate_string(n): letters_digits = string.ascii_letters + string.digits result = "" for i in range(n): - result += random.choice(letters_digits) + result += secrets.choice(letters_digits) return result def extract_remote_ip(request) -> str: if request.headers.get("CF-Connecting-IP"): - return cast(str, request.headers.get("Cf-Connecting-Ip")) + return cast(str, request.headers.get("CF-Connecting-IP")) elif request.headers.getlist("X-Forwarded-For"): return cast(str, request.headers.getlist("X-Forwarded-For")[0]) else: @@ -206,6 +233,60 @@ def compact_generate_response(response: Union[Mapping, Generator, RateLimitGener return Response(stream_with_context(generate()), status=200, mimetype="text/event-stream") +def length_prefixed_response(magic_number: int, response: Union[Mapping, Generator, RateLimitGenerator]) -> Response: + """ + This function is used to return a response with a length prefix. + Magic number is a one byte number that indicates the type of the response. + + For a compatibility with latest plugin daemon https://github.com/langgenius/dify-plugin-daemon/pull/341 + Avoid using line-based response, it leads a memory issue. + + We uses following format: + | Field | Size | Description | + |---------------|----------|---------------------------------| + | Magic Number | 1 byte | Magic number identifier | + | Reserved | 1 byte | Reserved field | + | Header Length | 2 bytes | Header length (usually 0xa) | + | Data Length | 4 bytes | Length of the data | + | Reserved | 6 bytes | Reserved fields | + | Data | Variable | Actual data content | + + | Reserved Fields | Header | Data | + |-----------------|----------|----------| + | 4 bytes total | Variable | Variable | + + all data is in little endian + """ + + def pack_response_with_length_prefix(response: bytes) -> bytes: + header_length = 0xA + data_length = len(response) + # | Magic Number 1byte | Reserved 1byte | Header Length 2bytes | Data Length 4bytes | Reserved 6bytes | Data + return struct.pack(" Generator: + for chunk in response: + if isinstance(chunk, str): + yield pack_response_with_length_prefix(chunk.encode("utf-8")) + else: + yield pack_response_with_length_prefix(chunk) + + return Response(stream_with_context(generate()), status=200, mimetype="text/event-stream") + + class TokenManager: @classmethod def generate_token( diff --git a/api/libs/jsonutil.py b/api/libs/jsonutil.py new file mode 100644 index 0000000000..fa29671034 --- /dev/null +++ b/api/libs/jsonutil.py @@ -0,0 +1,11 @@ +import json + +from pydantic import BaseModel + + +class PydanticModelEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, BaseModel): + return o.model_dump() + else: + super().default(o) diff --git a/api/libs/passport.py b/api/libs/passport.py index 8df4f529bc..fe8fc33b5f 100644 --- a/api/libs/passport.py +++ b/api/libs/passport.py @@ -14,9 +14,11 @@ class PassportService: def verify(self, token): try: return jwt.decode(token, self.sk, algorithms=["HS256"]) + except jwt.exceptions.ExpiredSignatureError: + raise Unauthorized("Token has expired.") except jwt.exceptions.InvalidSignatureError: raise Unauthorized("Invalid token signature.") except jwt.exceptions.DecodeError: raise Unauthorized("Invalid token.") - except jwt.exceptions.ExpiredSignatureError: - raise Unauthorized("Token has expired.") + except jwt.exceptions.PyJWTError: # Catch-all for other JWT errors + raise Unauthorized("Invalid token.") diff --git a/api/libs/sendgrid.py b/api/libs/sendgrid.py new file mode 100644 index 0000000000..5409e3eeeb --- /dev/null +++ b/api/libs/sendgrid.py @@ -0,0 +1,45 @@ +import logging + +import sendgrid # type: ignore +from python_http_client.exceptions import ForbiddenError, UnauthorizedError +from sendgrid.helpers.mail import Content, Email, Mail, To # type: ignore + + +class SendGridClient: + def __init__(self, sendgrid_api_key: str, _from: str): + self.sendgrid_api_key = sendgrid_api_key + self._from = _from + + def send(self, mail: dict): + logging.debug("Sending email with SendGrid") + + try: + _to = mail["to"] + + if not _to: + raise ValueError("SendGridClient: Cannot send email: recipient address is missing.") + + sg = sendgrid.SendGridAPIClient(api_key=self.sendgrid_api_key) + from_email = Email(self._from) + to_email = To(_to) + subject = mail["subject"] + content = Content("text/html", mail["html"]) + mail = Mail(from_email, to_email, subject, content) + mail_json = mail.get() # type: ignore + response = sg.client.mail.send.post(request_body=mail_json) + logging.debug(response.status_code) + logging.debug(response.body) + logging.debug(response.headers) + + except TimeoutError as e: + logging.exception("SendGridClient Timeout occurred while sending email") + raise + except (UnauthorizedError, ForbiddenError) as e: + logging.exception( + "SendGridClient Authentication failed. " + "Verify that your credentials and the 'from' email address are correct" + ) + raise + except Exception as e: + logging.exception(f"SendGridClient Unexpected error occurred while sending email to {_to}") + raise diff --git a/api/libs/smtp.py b/api/libs/smtp.py index 35561f071c..b94386660e 100644 --- a/api/libs/smtp.py +++ b/api/libs/smtp.py @@ -22,7 +22,11 @@ class SMTPClient: if self.use_tls: if self.opportunistic_tls: smtp = smtplib.SMTP(self.server, self.port, timeout=10) + # Send EHLO command with the HELO domain name as the server address + smtp.ehlo(self.server) smtp.starttls() + # Resend EHLO command to identify the TLS session + smtp.ehlo(self.server) else: smtp = smtplib.SMTP_SSL(self.server, self.port, timeout=10) else: diff --git a/api/libs/uuid_utils.py b/api/libs/uuid_utils.py new file mode 100644 index 0000000000..a8190011ed --- /dev/null +++ b/api/libs/uuid_utils.py @@ -0,0 +1,164 @@ +import secrets +import struct +import time +import uuid + +# Reference for UUIDv7 specification: +# RFC 9562, Section 5.7 - https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7 + +# Define the format for packing the timestamp as an unsigned 64-bit integer (big-endian). +# +# For details on the `struct.pack` format, refer to: +# https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment +_PACK_TIMESTAMP = ">Q" + +# Define the format for packing the 12-bit random data A (as specified in RFC 9562 Section 5.7) +# into an unsigned 16-bit integer (big-endian). +_PACK_RAND_A = ">H" + + +def _create_uuidv7_bytes(timestamp_ms: int, random_bytes: bytes) -> bytes: + """Create UUIDv7 byte structure with given timestamp and random bytes. + + This is a private helper function that handles the common logic for creating + UUIDv7 byte structure according to RFC 9562 specification. + + UUIDv7 Structure: + - 48 bits: timestamp (milliseconds since Unix epoch) + - 12 bits: random data A (with version bits) + - 62 bits: random data B (with variant bits) + + The function performs the following operations: + 1. Creates a 128-bit (16-byte) UUID structure + 2. Packs the timestamp into the first 48 bits (6 bytes) + 3. Sets the version bits to 7 (0111) in the correct position + 4. Sets the variant bits to 10 (binary) in the correct position + 5. Fills the remaining bits with the provided random bytes + + Args: + timestamp_ms: The timestamp in milliseconds since Unix epoch (48 bits). + random_bytes: Random bytes to use for the random portions (must be 10 bytes). + First 2 bytes are used for random data A (12 bits after version). + Last 8 bytes are used for random data B (62 bits after variant). + + Returns: + A 16-byte bytes object representing the complete UUIDv7 structure. + + Note: + This function assumes the random_bytes parameter is exactly 10 bytes. + The caller is responsible for providing appropriate random data. + """ + # Create the 128-bit UUID structure + uuid_bytes = bytearray(16) + + # Pack timestamp (48 bits) into first 6 bytes + uuid_bytes[0:6] = struct.pack(_PACK_TIMESTAMP, timestamp_ms)[2:8] # Take last 6 bytes of 8-byte big-endian + + # Next 16 bits: random data A (12 bits) + version (4 bits) + # Take first 2 random bytes and set version to 7 + rand_a = struct.unpack(_PACK_RAND_A, random_bytes[0:2])[0] + # Clear the highest 4 bits to make room for the version field + # by performing a bitwise AND with 0x0FFF (binary: 0b0000_1111_1111_1111). + rand_a = rand_a & 0x0FFF + # Set the version field to 7 (binary: 0111) by performing a bitwise OR with 0x7000 (binary: 0b0111_0000_0000_0000). + rand_a = rand_a | 0x7000 + uuid_bytes[6:8] = struct.pack(_PACK_RAND_A, rand_a) + + # Last 64 bits: random data B (62 bits) + variant (2 bits) + # Use remaining 8 random bytes and set variant to 10 (binary) + uuid_bytes[8:16] = random_bytes[2:10] + + # Set variant bits (first 2 bits of byte 8 should be '10') + uuid_bytes[8] = (uuid_bytes[8] & 0x3F) | 0x80 # Set variant to 10xxxxxx + + return bytes(uuid_bytes) + + +def uuidv7(timestamp_ms: int | None = None) -> uuid.UUID: + """Generate a UUID version 7 according to RFC 9562 specification. + + UUIDv7 features a time-ordered value field derived from the widely + implemented and well known Unix Epoch timestamp source, the number of + milliseconds since midnight 1 Jan 1970 UTC, leap seconds excluded. + + Structure: + - 48 bits: timestamp (milliseconds since Unix epoch) + - 12 bits: random data A (with version bits) + - 62 bits: random data B (with variant bits) + + Args: + timestamp_ms: The timestamp used when generating UUID, use the current time if unspecified. + Should be an integer representing milliseconds since Unix epoch. + + Returns: + A UUID object representing a UUIDv7. + + Example: + >>> import time + >>> # Generate UUIDv7 with current time + >>> uuid_current = uuidv7() + >>> # Generate UUIDv7 with specific timestamp + >>> uuid_specific = uuidv7(int(time.time() * 1000)) + """ + if timestamp_ms is None: + timestamp_ms = int(time.time() * 1000) + + # Generate 10 random bytes for the random portions + random_bytes = secrets.token_bytes(10) + + # Create UUIDv7 bytes using the helper function + uuid_bytes = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + return uuid.UUID(bytes=uuid_bytes) + + +def uuidv7_timestamp(id_: uuid.UUID) -> int: + """Extract the timestamp from a UUIDv7. + + UUIDv7 contains a 48-bit timestamp field representing milliseconds since + the Unix epoch (1970-01-01 00:00:00 UTC). This function extracts and + returns that timestamp as an integer representing milliseconds since the epoch. + + Args: + id_: A UUID object that should be a UUIDv7 (version 7). + + Returns: + The timestamp as an integer representing milliseconds since Unix epoch. + + Raises: + ValueError: If the provided UUID is not version 7. + + Example: + >>> uuid_v7 = uuidv7() + >>> timestamp = uuidv7_timestamp(uuid_v7) + >>> print(f"UUID was created at: {timestamp} ms") + """ + # Verify this is a UUIDv7 + if id_.version != 7: + raise ValueError(f"Expected UUIDv7 (version 7), got version {id_.version}") + + # Extract the UUID bytes + uuid_bytes = id_.bytes + + # Extract the first 48 bits (6 bytes) as the timestamp in milliseconds + # Pad with 2 zero bytes at the beginning to make it 8 bytes for unpacking as Q (unsigned long long) + timestamp_bytes = b"\x00\x00" + uuid_bytes[0:6] + ts_in_ms = struct.unpack(_PACK_TIMESTAMP, timestamp_bytes)[0] + + # Return timestamp directly in milliseconds as integer + assert isinstance(ts_in_ms, int) + return ts_in_ms + + +def uuidv7_boundary(timestamp_ms: int) -> uuid.UUID: + """Generate a non-random uuidv7 with the given timestamp (first 48 bits) and + all random bits to 0. As the smallest possible uuidv7 for that timestamp, + it may be used as a boundary for partitions. + """ + # Use zero bytes for all random portions + zero_random_bytes = b"\x00" * 10 + + # Create UUIDv7 bytes using the helper function + uuid_bytes = _create_uuidv7_bytes(timestamp_ms, zero_random_bytes) + + return uuid.UUID(bytes=uuid_bytes) diff --git a/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py new file mode 100644 index 0000000000..d7a5d116c9 --- /dev/null +++ b/api/migrations/versions/2025_06_06_1424-4474872b0ee6_workflow_draft_varaibles_add_node_execution_id.py @@ -0,0 +1,60 @@ +"""`workflow_draft_varaibles` add `node_execution_id` column, add an index for `workflow_node_executions`. + +Revision ID: 4474872b0ee6 +Revises: 2adcbe1f5dfb +Create Date: 2025-06-06 14:24:44.213018 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4474872b0ee6' +down_revision = '2adcbe1f5dfb' +branch_labels = None +depends_on = None + + +def upgrade(): + # `CREATE INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block` + # context manager to wrap the index creation statement. + # Reference: + # + # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block + with op.get_context().autocommit_block(): + op.create_index( + op.f('workflow_node_executions_tenant_id_idx'), + "workflow_node_executions", + ['tenant_id', 'workflow_id', 'node_id', sa.literal_column('created_at DESC')], + unique=False, + postgresql_concurrently=True, + ) + + with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op: + batch_op.add_column(sa.Column('node_execution_id', models.types.StringUUID(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so use the `autocommit_block` + # context manager to wrap the index creation statement. + # Reference: + # + # - https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + # - https://alembic.sqlalchemy.org/en/latest/api/runtime.html#alembic.runtime.migration.MigrationContext.autocommit_block + # `DROP INDEX CONCURRENTLY` cannot run within a transaction, so commit existing transactions first. + # Reference: + # + # https://www.postgresql.org/docs/current/sql-createindex.html#:~:text=Another%20difference%20is,CREATE%20INDEX%20CONCURRENTLY%20cannot. + with op.get_context().autocommit_block(): + op.drop_index(op.f('workflow_node_executions_tenant_id_idx'), postgresql_concurrently=True) + + with op.batch_alter_table('workflow_draft_variables', schema=None) as batch_op: + batch_op.drop_column('node_execution_id') + + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_06_19_1633-0ab65e1cc7fa_remove_sequence_number_from_workflow_.py b/api/migrations/versions/2025_06_19_1633-0ab65e1cc7fa_remove_sequence_number_from_workflow_.py new file mode 100644 index 0000000000..29fef77798 --- /dev/null +++ b/api/migrations/versions/2025_06_19_1633-0ab65e1cc7fa_remove_sequence_number_from_workflow_.py @@ -0,0 +1,66 @@ +"""remove sequence_number from workflow_runs + +Revision ID: 0ab65e1cc7fa +Revises: 4474872b0ee6 +Create Date: 2025-06-19 16:33:13.377215 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0ab65e1cc7fa' +down_revision = '4474872b0ee6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('workflow_runs', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('workflow_run_tenant_app_sequence_idx')) + batch_op.drop_column('sequence_number') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # WARNING: This downgrade CANNOT recover the original sequence_number values! + # The original sequence numbers are permanently lost after the upgrade. + # This downgrade will regenerate sequence numbers based on created_at order, + # which may result in different values than the original sequence numbers. + # + # If you need to preserve original sequence numbers, use the alternative + # migration approach that creates a backup table before removal. + + # Step 1: Add sequence_number column as nullable first + with op.batch_alter_table('workflow_runs', schema=None) as batch_op: + batch_op.add_column(sa.Column('sequence_number', sa.INTEGER(), autoincrement=False, nullable=True)) + + # Step 2: Populate sequence_number values based on created_at order within each app + # NOTE: This recreates sequence numbering logic but values will be different + # from the original sequence numbers that were removed in the upgrade + connection = op.get_bind() + connection.execute(sa.text(""" + UPDATE workflow_runs + SET sequence_number = subquery.row_num + FROM ( + SELECT id, ROW_NUMBER() OVER ( + PARTITION BY tenant_id, app_id + ORDER BY created_at, id + ) as row_num + FROM workflow_runs + ) subquery + WHERE workflow_runs.id = subquery.id + """)) + + # Step 3: Make the column NOT NULL and add the index + with op.batch_alter_table('workflow_runs', schema=None) as batch_op: + batch_op.alter_column('sequence_number', nullable=False) + batch_op.create_index(batch_op.f('workflow_run_tenant_app_sequence_idx'), ['tenant_id', 'app_id', 'sequence_number'], unique=False) + + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py b/api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py new file mode 100644 index 0000000000..0548bf05ef --- /dev/null +++ b/api/migrations/versions/2025_06_25_0936-58eb7bdb93fe_add_mcp_server_tool_and_app_server.py @@ -0,0 +1,64 @@ +"""add mcp server tool and app server + +Revision ID: 58eb7bdb93fe +Revises: 0ab65e1cc7fa +Create Date: 2025-06-25 09:36:07.510570 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '58eb7bdb93fe' +down_revision = '0ab65e1cc7fa' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('app_mcp_servers', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.String(length=255), nullable=False), + sa.Column('server_code', sa.String(length=255), nullable=False), + sa.Column('status', sa.String(length=255), server_default=sa.text("'normal'::character varying"), nullable=False), + sa.Column('parameters', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.PrimaryKeyConstraint('id', name='app_mcp_server_pkey'), + sa.UniqueConstraint('tenant_id', 'app_id', name='unique_app_mcp_server_tenant_app_id'), + sa.UniqueConstraint('server_code', name='unique_app_mcp_server_server_code') + ) + op.create_table('tool_mcp_providers', + sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('name', sa.String(length=40), nullable=False), + sa.Column('server_identifier', sa.String(length=24), nullable=False), + sa.Column('server_url', sa.Text(), nullable=False), + sa.Column('server_url_hash', sa.String(length=64), nullable=False), + sa.Column('icon', sa.String(length=255), nullable=True), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('user_id', models.types.StringUUID(), nullable=False), + sa.Column('encrypted_credentials', sa.Text(), nullable=True), + sa.Column('authed', sa.Boolean(), nullable=False), + sa.Column('tools', sa.Text(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False), + sa.PrimaryKeyConstraint('id', name='tool_mcp_provider_pkey'), + sa.UniqueConstraint('tenant_id', 'name', name='unique_mcp_provider_name'), + sa.UniqueConstraint('tenant_id', 'server_identifier', name='unique_mcp_provider_server_identifier'), + sa.UniqueConstraint('tenant_id', 'server_url_hash', name='unique_mcp_provider_server_url') + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('tool_mcp_providers') + op.drop_table('app_mcp_servers') + # ### end Alembic commands ### diff --git a/api/migrations/versions/2025_07_02_2332-1c9ba48be8e4_add_uuidv7_function_in_sql.py b/api/migrations/versions/2025_07_02_2332-1c9ba48be8e4_add_uuidv7_function_in_sql.py new file mode 100644 index 0000000000..2bbbb3d28e --- /dev/null +++ b/api/migrations/versions/2025_07_02_2332-1c9ba48be8e4_add_uuidv7_function_in_sql.py @@ -0,0 +1,86 @@ +"""add uuidv7 function in SQL + +Revision ID: 1c9ba48be8e4 +Revises: 58eb7bdb93fe +Create Date: 2025-07-02 23:32:38.484499 + +""" + +""" +The functions in this files comes from https://github.com/dverite/postgres-uuidv7-sql/, with minor modifications. + +LICENSE: + +# Copyright and License + +Copyright (c) 2024, Daniel Vérité + +Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. + +In no event shall Daniel Vérité be liable to any party for direct, indirect, special, incidental, or consequential damages, including lost profits, arising out of the use of this software and its documentation, even if Daniel Vérité has been advised of the possibility of such damage. + +Daniel Vérité specifically disclaims any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an "AS IS" basis, and Daniel Vérité has no obligations to provide maintenance, support, updates, enhancements, or modifications. +""" + +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1c9ba48be8e4' +down_revision = '58eb7bdb93fe' +branch_labels: None = None +depends_on: None = None + + +def upgrade(): + # This implementation differs slightly from the original uuidv7 function in + # https://github.com/dverite/postgres-uuidv7-sql/. + # The ability to specify source timestamp has been removed because its type signature is incompatible with + # PostgreSQL 18's `uuidv7` function. This capability is rarely needed in practice, as IDs can be + # generated and controlled within the application layer. + op.execute(sa.text(r""" +/* Main function to generate a uuidv7 value with millisecond precision */ +CREATE FUNCTION uuidv7() RETURNS uuid +AS +$$ + -- Replace the first 48 bits of a uuidv4 with the current + -- number of milliseconds since 1970-01-01 UTC + -- and set the "ver" field to 7 by setting additional bits +SELECT encode( + set_bit( + set_bit( + overlay(uuid_send(gen_random_uuid()) placing + substring(int8send((extract(epoch from clock_timestamp()) * 1000)::bigint) from + 3) + from 1 for 6), + 52, 1), + 53, 1), 'hex')::uuid; +$$ LANGUAGE SQL VOLATILE PARALLEL SAFE; + +COMMENT ON FUNCTION uuidv7 IS + 'Generate a uuid-v7 value with a 48-bit timestamp (millisecond precision) and 74 bits of randomness'; +""")) + + op.execute(sa.text(r""" +CREATE FUNCTION uuidv7_boundary(timestamptz) RETURNS uuid +AS +$$ + /* uuid fields: version=0b0111, variant=0b10 */ +SELECT encode( + overlay('\x00000000000070008000000000000000'::bytea + placing substring(int8send(floor(extract(epoch from $1) * 1000)::bigint) from 3) + from 1 for 6), + 'hex')::uuid; +$$ LANGUAGE SQL STABLE STRICT PARALLEL SAFE; + +COMMENT ON FUNCTION uuidv7_boundary(timestamptz) IS + 'Generate a non-random uuidv7 with the given timestamp (first 48 bits) and all random bits to 0. As the smallest possible uuidv7 for that timestamp, it may be used as a boundary for partitions.'; +""" +)) + + +def downgrade(): + op.execute(sa.text("DROP FUNCTION uuidv7")) + op.execute(sa.text("DROP FUNCTION uuidv7_boundary")) diff --git a/api/models/__init__.py b/api/models/__init__.py index 83b50eb099..1b4bdd32e4 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -34,6 +34,7 @@ from .model import ( App, AppAnnotationHitHistory, AppAnnotationSetting, + AppMCPServer, AppMode, AppModelConfig, Conversation, @@ -103,6 +104,7 @@ __all__ = [ "AppAnnotationHitHistory", "AppAnnotationSetting", "AppDatasetJoin", + "AppMCPServer", # Added "AppMode", "AppModelConfig", "BuiltinToolProvider", diff --git a/api/models/_workflow_exc.py b/api/models/_workflow_exc.py new file mode 100644 index 0000000000..f6271bda47 --- /dev/null +++ b/api/models/_workflow_exc.py @@ -0,0 +1,20 @@ +"""All these exceptions are not meant to be caught by callers.""" + + +class WorkflowDataError(Exception): + """Base class for all workflow data related exceptions. + + This should be used to indicate issues with workflow data integrity, such as + no `graph` configuration, missing `nodes` field in `graph` configuration, or + similar issues. + """ + + pass + + +class NodeNotFoundError(WorkflowDataError): + """Raised when a node with the specified ID is not found in the workflow.""" + + def __init__(self, node_id: str): + super().__init__(f"Node with ID '{node_id}' not found in the workflow.") + self.node_id = node_id diff --git a/api/models/dataset.py b/api/models/dataset.py index ad43d6f371..1ec27203a0 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -140,7 +140,7 @@ class Dataset(Base): def word_count(self): return ( db.session.query(Document) - .with_entities(func.coalesce(func.sum(Document.word_count))) + .with_entities(func.coalesce(func.sum(Document.word_count), 0)) .filter(Document.dataset_id == self.id) .scalar() ) @@ -448,7 +448,7 @@ class Document(Base): def hit_count(self): return ( db.session.query(DocumentSegment) - .with_entities(func.coalesce(func.sum(DocumentSegment.hit_count))) + .with_entities(func.coalesce(func.sum(DocumentSegment.hit_count), 0)) .filter(DocumentSegment.document_id == self.id) .scalar() ) diff --git a/api/models/enums.py b/api/models/enums.py index 4434c3fec8..cc9f28a7bb 100644 --- a/api/models/enums.py +++ b/api/models/enums.py @@ -21,3 +21,12 @@ class DraftVariableType(StrEnum): NODE = "node" SYS = "sys" CONVERSATION = "conversation" + + +class MessageStatus(StrEnum): + """ + Message Status Enum + """ + + NORMAL = "normal" + ERROR = "error" diff --git a/api/models/model.py b/api/models/model.py index 229e77134e..7e9e91727d 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -10,7 +10,6 @@ from core.plugin.entities.plugin import GenericProviderID from core.tools.entities.tool_entities import ToolProviderType from core.tools.signature import sign_tool_file from core.workflow.entities.workflow_execution import WorkflowExecutionStatus -from services.plugin.plugin_service import PluginService if TYPE_CHECKING: from models.workflow import Workflow @@ -51,7 +50,6 @@ class AppMode(StrEnum): CHAT = "chat" ADVANCED_CHAT = "advanced-chat" AGENT_CHAT = "agent-chat" - CHANNEL = "channel" @classmethod def value_of(cls, value: str) -> "AppMode": @@ -169,6 +167,7 @@ class App(Base): @property def deleted_tools(self) -> list: from core.tools.tool_manager import ToolManager + from services.plugin.plugin_service import PluginService # get agent mode tools app_model_config = self.app_model_config @@ -611,6 +610,14 @@ class InstalledApp(Base): return tenant +class ConversationSource(StrEnum): + """This enumeration is designed for use with `Conversation.from_source`.""" + + # NOTE(QuantumGhost): The enumeration members may not cover all possible cases. + API = "api" + CONSOLE = "console" + + class Conversation(Base): __tablename__ = "conversations" __table_args__ = ( @@ -632,7 +639,14 @@ class Conversation(Base): system_instruction = db.Column(db.Text) system_instruction_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) status = db.Column(db.String(255), nullable=False) + + # The `invoke_from` records how the conversation is created. + # + # Its value corresponds to the members of `InvokeFrom`. + # (api/core/app/entities/app_invoke_entities.py) invoke_from = db.Column(db.String(255), nullable=True) + + # ref: ConversationSource. from_source = db.Column(db.String(255), nullable=False) from_end_user_id = db.Column(StringUUID) from_account_id = db.Column(StringUUID) @@ -661,7 +675,7 @@ class Conversation(Base): if isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY: if value["transfer_method"] == FileTransferMethod.TOOL_FILE: value["tool_file_id"] = value["related_id"] - elif value["transfer_method"] == FileTransferMethod.LOCAL_FILE: + elif value["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]: value["upload_file_id"] = value["related_id"] inputs[key] = file_factory.build_from_mapping(mapping=value, tenant_id=value["tenant_id"]) elif isinstance(value, list) and all( @@ -671,7 +685,7 @@ class Conversation(Base): for item in value: if item["transfer_method"] == FileTransferMethod.TOOL_FILE: item["tool_file_id"] = item["related_id"] - elif item["transfer_method"] == FileTransferMethod.LOCAL_FILE: + elif item["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]: item["upload_file_id"] = item["related_id"] inputs[key].append(file_factory.build_from_mapping(mapping=item, tenant_id=item["tenant_id"])) @@ -703,7 +717,6 @@ class Conversation(Base): if "model" in override_model_configs: app_model_config = AppModelConfig() app_model_config = app_model_config.from_model_config_dict(override_model_configs) - assert app_model_config is not None, "app model config not found" model_config = app_model_config.to_dict() else: model_config["configs"] = override_model_configs @@ -817,7 +830,12 @@ class Conversation(Base): @property def first_message(self): - return db.session.query(Message).filter(Message.conversation_id == self.id).first() + return ( + db.session.query(Message) + .filter(Message.conversation_id == self.id) + .order_by(Message.created_at.asc()) + .first() + ) @property def app(self): @@ -894,11 +912,11 @@ class Message(Base): _inputs: Mapped[dict] = mapped_column("inputs", db.JSON) query: Mapped[str] = db.Column(db.Text, nullable=False) message = db.Column(db.JSON, nullable=False) - message_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) + message_tokens: Mapped[int] = db.Column(db.Integer, nullable=False, server_default=db.text("0")) message_unit_price = db.Column(db.Numeric(10, 4), nullable=False) message_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) answer: Mapped[str] = db.Column(db.Text, nullable=False) - answer_tokens = db.Column(db.Integer, nullable=False, server_default=db.text("0")) + answer_tokens: Mapped[int] = db.Column(db.Integer, nullable=False, server_default=db.text("0")) answer_unit_price = db.Column(db.Numeric(10, 4), nullable=False) answer_price_unit = db.Column(db.Numeric(10, 7), nullable=False, server_default=db.text("0.001")) parent_message_id = db.Column(StringUUID, nullable=True) @@ -915,7 +933,7 @@ class Message(Base): created_at: Mapped[datetime] = mapped_column(db.DateTime, server_default=func.current_timestamp()) updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) agent_based = db.Column(db.Boolean, nullable=False, server_default=db.text("false")) - workflow_run_id = db.Column(StringUUID) + workflow_run_id: Mapped[str] = db.Column(StringUUID) @property def inputs(self): @@ -927,7 +945,7 @@ class Message(Base): if isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY: if value["transfer_method"] == FileTransferMethod.TOOL_FILE: value["tool_file_id"] = value["related_id"] - elif value["transfer_method"] == FileTransferMethod.LOCAL_FILE: + elif value["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]: value["upload_file_id"] = value["related_id"] inputs[key] = file_factory.build_from_mapping(mapping=value, tenant_id=value["tenant_id"]) elif isinstance(value, list) and all( @@ -937,7 +955,7 @@ class Message(Base): for item in value: if item["transfer_method"] == FileTransferMethod.TOOL_FILE: item["tool_file_id"] = item["related_id"] - elif item["transfer_method"] == FileTransferMethod.LOCAL_FILE: + elif item["transfer_method"] in [FileTransferMethod.LOCAL_FILE, FileTransferMethod.REMOTE_URL]: item["upload_file_id"] = item["related_id"] inputs[key].append(file_factory.build_from_mapping(mapping=item, tenant_id=item["tenant_id"])) return inputs @@ -1437,6 +1455,39 @@ class EndUser(Base, UserMixin): updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) +class AppMCPServer(Base): + __tablename__ = "app_mcp_servers" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="app_mcp_server_pkey"), + db.UniqueConstraint("tenant_id", "app_id", name="unique_app_mcp_server_tenant_app_id"), + db.UniqueConstraint("server_code", name="unique_app_mcp_server_server_code"), + ) + id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()")) + tenant_id = db.Column(StringUUID, nullable=False) + app_id = db.Column(StringUUID, nullable=False) + name = db.Column(db.String(255), nullable=False) + description = db.Column(db.String(255), nullable=False) + server_code = db.Column(db.String(255), nullable=False) + status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + parameters = db.Column(db.Text, nullable=False) + + created_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + + @staticmethod + def generate_server_code(n): + while True: + result = generate_string(n) + while db.session.query(AppMCPServer).filter(AppMCPServer.server_code == result).count() > 0: + result = generate_string(n) + + return result + + @property + def parameters_dict(self) -> dict[str, Any]: + return cast(dict[str, Any], json.loads(self.parameters)) + + class Site(Base): __tablename__ = "sites" __table_args__ = ( diff --git a/api/models/tools.py b/api/models/tools.py index 03fbc3acb1..9d2c3baea5 100644 --- a/api/models/tools.py +++ b/api/models/tools.py @@ -1,12 +1,16 @@ import json from datetime import datetime from typing import Any, cast +from urllib.parse import urlparse import sqlalchemy as sa from deprecated import deprecated from sqlalchemy import ForeignKey, func from sqlalchemy.orm import Mapped, mapped_column +from core.file import helpers as file_helpers +from core.helper import encrypter +from core.mcp.types import Tool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration @@ -189,6 +193,108 @@ class WorkflowToolProvider(Base): return db.session.query(App).filter(App.id == self.app_id).first() +class MCPToolProvider(Base): + """ + The table stores the mcp providers. + """ + + __tablename__ = "tool_mcp_providers" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="tool_mcp_provider_pkey"), + db.UniqueConstraint("tenant_id", "server_url_hash", name="unique_mcp_provider_server_url"), + db.UniqueConstraint("tenant_id", "name", name="unique_mcp_provider_name"), + db.UniqueConstraint("tenant_id", "server_identifier", name="unique_mcp_provider_server_identifier"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) + # name of the mcp provider + name: Mapped[str] = mapped_column(db.String(40), nullable=False) + # server identifier of the mcp provider + server_identifier: Mapped[str] = mapped_column(db.String(24), nullable=False) + # encrypted url of the mcp provider + server_url: Mapped[str] = mapped_column(db.Text, nullable=False) + # hash of server_url for uniqueness check + server_url_hash: Mapped[str] = mapped_column(db.String(64), nullable=False) + # icon of the mcp provider + icon: Mapped[str] = mapped_column(db.String(255), nullable=True) + # tenant id + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + # who created this tool + user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + # encrypted credentials + encrypted_credentials: Mapped[str] = mapped_column(db.Text, nullable=True) + # authed + authed: Mapped[bool] = mapped_column(db.Boolean, nullable=False, default=False) + # tools + tools: Mapped[str] = mapped_column(db.Text, nullable=False, default="[]") + created_at: Mapped[datetime] = mapped_column( + db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + ) + updated_at: Mapped[datetime] = mapped_column( + db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)") + ) + + def load_user(self) -> Account | None: + return db.session.query(Account).filter(Account.id == self.user_id).first() + + @property + def tenant(self) -> Tenant | None: + return db.session.query(Tenant).filter(Tenant.id == self.tenant_id).first() + + @property + def credentials(self) -> dict: + try: + return cast(dict, json.loads(self.encrypted_credentials)) or {} + except Exception: + return {} + + @property + def mcp_tools(self) -> list[Tool]: + return [Tool(**tool) for tool in json.loads(self.tools)] + + @property + def provider_icon(self) -> dict[str, str] | str: + try: + return cast(dict[str, str], json.loads(self.icon)) + except json.JSONDecodeError: + return file_helpers.get_signed_file_url(self.icon) + + @property + def decrypted_server_url(self) -> str: + return cast(str, encrypter.decrypt_token(self.tenant_id, self.server_url)) + + @property + def masked_server_url(self) -> str: + def mask_url(url: str, mask_char: str = "*") -> str: + """ + mask the url to a simple string + """ + parsed = urlparse(url) + base_url = f"{parsed.scheme}://{parsed.netloc}" + + if parsed.path and parsed.path != "/": + return f"{base_url}/{mask_char * 6}" + else: + return base_url + + return mask_url(self.decrypted_server_url) + + @property + def decrypted_credentials(self) -> dict: + from core.tools.mcp_tool.provider import MCPToolProviderController + from core.tools.utils.configuration import ProviderConfigEncrypter + + provider_controller = MCPToolProviderController._from_db(self) + + tool_configuration = ProviderConfigEncrypter( + tenant_id=self.tenant_id, + config=list(provider_controller.get_credentials_schema()), + provider_type=provider_controller.provider_type.value, + provider_identity=provider_controller.provider_id, + ) + return tool_configuration.decrypt(self.credentials, use_cache=False) + + class ToolModelInvoke(Base): """ store the invoke logs from tool invoke diff --git a/api/models/workflow.py b/api/models/workflow.py index e868fb77a7..9930859201 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -7,17 +7,25 @@ from typing import TYPE_CHECKING, Any, Optional, Union from uuid import uuid4 from flask_login import current_user +from sqlalchemy import orm +from core.file.constants import maybe_file_object +from core.file.models import File from core.variables import utils as variable_utils +from core.variables.variables import FloatVariable, IntegerVariable, StringVariable from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID -from factories.variable_factory import build_segment +from core.workflow.nodes.enums import NodeType +from factories.variable_factory import TypeMismatchError, build_segment_with_type +from libs.helper import extract_tenant_id + +from ._workflow_exc import NodeNotFoundError, WorkflowDataError if TYPE_CHECKING: from models.model import AppMode import sqlalchemy as sa -from sqlalchemy import UniqueConstraint, func -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Index, PrimaryKeyConstraint, UniqueConstraint, func +from sqlalchemy.orm import Mapped, declared_attr, mapped_column from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from core.helper import encrypter @@ -72,6 +80,10 @@ class WorkflowType(Enum): return cls.WORKFLOW if app_mode == AppMode.WORKFLOW else cls.CHAT +class _InvalidGraphDefinitionError(Exception): + pass + + class Workflow(Base): """ Workflow, for `Workflow App` and `Chat App workflow mode`. @@ -136,6 +148,8 @@ class Workflow(Base): "conversation_variables", db.Text, nullable=False, server_default="{}" ) + VERSION_DRAFT = "draft" + @classmethod def new( cls, @@ -179,8 +193,72 @@ class Workflow(Base): @property def graph_dict(self) -> Mapping[str, Any]: + # TODO(QuantumGhost): Consider caching `graph_dict` to avoid repeated JSON decoding. + # + # Using `functools.cached_property` could help, but some code in the codebase may + # modify the returned dict, which can cause issues elsewhere. + # + # For example, changing this property to a cached property led to errors like the + # following when single stepping an `Iteration` node: + # + # Root node id 1748401971780start not found in the graph + # + # There is currently no standard way to make a dict deeply immutable in Python, + # and tracking modifications to the returned dict is difficult. For now, we leave + # the code as-is to avoid these issues. + # + # Currently, the following functions / methods would mutate the returned dict: + # + # - `_get_graph_and_variable_pool_of_single_iteration`. + # - `_get_graph_and_variable_pool_of_single_loop`. return json.loads(self.graph) if self.graph else {} + def get_node_config_by_id(self, node_id: str) -> Mapping[str, Any]: + """Extract a node configuration from the workflow graph by node ID. + A node configuration is a dictionary containing the node's properties, including + the node's id, title, and its data as a dict. + """ + workflow_graph = self.graph_dict + + if not workflow_graph: + raise WorkflowDataError(f"workflow graph not found, workflow_id={self.id}") + + nodes = workflow_graph.get("nodes") + if not nodes: + raise WorkflowDataError("nodes not found in workflow graph") + + try: + node_config = next(filter(lambda node: node["id"] == node_id, nodes)) + except StopIteration: + raise NodeNotFoundError(node_id) + assert isinstance(node_config, dict) + return node_config + + @staticmethod + def get_node_type_from_node_config(node_config: Mapping[str, Any]) -> NodeType: + """Extract type of a node from the node configuration returned by `get_node_config_by_id`.""" + node_config_data = node_config.get("data", {}) + # Get node class + node_type = NodeType(node_config_data.get("type")) + return node_type + + @staticmethod + def get_enclosing_node_type_and_id(node_config: Mapping[str, Any]) -> tuple[NodeType, str] | None: + in_loop = node_config.get("isInLoop", False) + in_iteration = node_config.get("isInIteration", False) + if in_loop: + loop_id = node_config.get("loop_id") + if loop_id is None: + raise _InvalidGraphDefinitionError("invalid graph") + return NodeType.LOOP, loop_id + elif in_iteration: + iteration_id = node_config.get("iteration_id") + if iteration_id is None: + raise _InvalidGraphDefinitionError("invalid graph") + return NodeType.ITERATION, iteration_id + else: + return None + @property def features(self) -> str: """ @@ -270,18 +348,13 @@ class Workflow(Base): ) @property - def environment_variables(self) -> Sequence[Variable]: + def environment_variables(self) -> Sequence[StringVariable | IntegerVariable | FloatVariable | SecretVariable]: # TODO: find some way to init `self._environment_variables` when instance created. if self._environment_variables is None: self._environment_variables = "{}" # Get tenant_id from current_user (Account or EndUser) - if isinstance(current_user, Account): - # Account user - tenant_id = current_user.current_tenant_id - else: - # EndUser - tenant_id = current_user.tenant_id + tenant_id = extract_tenant_id(current_user) if not tenant_id: return [] @@ -295,11 +368,15 @@ class Workflow(Base): def decrypt_func(var): if isinstance(var, SecretVariable): return var.model_copy(update={"value": encrypter.decrypt_token(tenant_id=tenant_id, token=var.value)}) - else: + elif isinstance(var, (StringVariable, IntegerVariable, FloatVariable)): return var + else: + raise AssertionError("this statement should be unreachable.") - results = list(map(decrypt_func, results)) - return results + decrypted_results: list[SecretVariable | StringVariable | IntegerVariable | FloatVariable] = list( + map(decrypt_func, results) + ) + return decrypted_results @environment_variables.setter def environment_variables(self, value: Sequence[Variable]): @@ -308,12 +385,7 @@ class Workflow(Base): return # Get tenant_id from current_user (Account or EndUser) - if isinstance(current_user, Account): - # Account user - tenant_id = current_user.current_tenant_id - else: - # EndUser - tenant_id = current_user.tenant_id + tenant_id = extract_tenant_id(current_user) if not tenant_id: self._environment_variables = "{}" @@ -376,6 +448,10 @@ class Workflow(Base): ensure_ascii=False, ) + @staticmethod + def version_from_datetime(d: datetime) -> str: + return str(d) + class WorkflowRun(Base): """ @@ -386,7 +462,7 @@ class WorkflowRun(Base): - id (uuid) Run ID - tenant_id (uuid) Workspace ID - app_id (uuid) App ID - - sequence_number (int) Auto-increment sequence number, incremented within the App, starting from 1 + - workflow_id (uuid) Workflow ID - type (string) Workflow type - triggered_from (string) Trigger source @@ -419,13 +495,12 @@ class WorkflowRun(Base): __table_args__ = ( db.PrimaryKeyConstraint("id", name="workflow_run_pkey"), db.Index("workflow_run_triggerd_from_idx", "tenant_id", "app_id", "triggered_from"), - db.Index("workflow_run_tenant_app_sequence_idx", "tenant_id", "app_id", "sequence_number"), ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) app_id: Mapped[str] = mapped_column(StringUUID) - sequence_number: Mapped[int] = mapped_column() + workflow_id: Mapped[str] = mapped_column(StringUUID) type: Mapped[str] = mapped_column(db.String(255)) triggered_from: Mapped[str] = mapped_column(db.String(255)) @@ -485,7 +560,6 @@ class WorkflowRun(Base): "id": self.id, "tenant_id": self.tenant_id, "app_id": self.app_id, - "sequence_number": self.sequence_number, "workflow_id": self.workflow_id, "type": self.type, "triggered_from": self.triggered_from, @@ -511,7 +585,6 @@ class WorkflowRun(Base): id=data.get("id"), tenant_id=data.get("tenant_id"), app_id=data.get("app_id"), - sequence_number=data.get("sequence_number"), workflow_id=data.get("workflow_id"), type=data.get("type"), triggered_from=data.get("triggered_from"), @@ -590,28 +663,48 @@ class WorkflowNodeExecutionModel(Base): """ __tablename__ = "workflow_node_executions" - __table_args__ = ( - db.PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"), - db.Index( - "workflow_node_execution_workflow_run_idx", - "tenant_id", - "app_id", - "workflow_id", - "triggered_from", - "workflow_run_id", - ), - db.Index( - "workflow_node_execution_node_run_idx", "tenant_id", "app_id", "workflow_id", "triggered_from", "node_id" - ), - db.Index( - "workflow_node_execution_id_idx", - "tenant_id", - "app_id", - "workflow_id", - "triggered_from", - "node_execution_id", - ), - ) + + @declared_attr + def __table_args__(cls): # noqa + return ( + PrimaryKeyConstraint("id", name="workflow_node_execution_pkey"), + Index( + "workflow_node_execution_workflow_run_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "workflow_run_id", + ), + Index( + "workflow_node_execution_node_run_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "node_id", + ), + Index( + "workflow_node_execution_id_idx", + "tenant_id", + "app_id", + "workflow_id", + "triggered_from", + "node_execution_id", + ), + Index( + # The first argument is the index name, + # which we leave as `None`` to allow auto-generation by the ORM. + None, + cls.tenant_id, + cls.workflow_id, + cls.node_id, + # MyPy may flag the following line because it doesn't recognize that + # the `declared_attr` decorator passes the receiving class as the first + # argument to this method, allowing us to reference class attributes. + cls.created_at.desc(), # type: ignore + ), + ) id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuid_generate_v4()")) tenant_id: Mapped[str] = mapped_column(StringUUID) @@ -818,8 +911,18 @@ def _naive_utc_datetime(): class WorkflowDraftVariable(Base): + """`WorkflowDraftVariable` record variables and outputs generated during + debugging worfklow or chatflow. + + IMPORTANT: This model maintains multiple invariant rules that must be preserved. + Do not instantiate this class directly with the constructor. + + Instead, use the factory methods (`new_conversation_variable`, `new_sys_variable`, + `new_node_variable`) defined below to ensure all invariants are properly maintained. + """ + @staticmethod - def unique_columns() -> list[str]: + def unique_app_id_node_id_name() -> list[str]: return [ "app_id", "node_id", @@ -827,7 +930,9 @@ class WorkflowDraftVariable(Base): ] __tablename__ = "workflow_draft_variables" - __table_args__ = (UniqueConstraint(*unique_columns()),) + __table_args__ = (UniqueConstraint(*unique_app_id_node_id_name()),) + # Required for instance variable annotation. + __allow_unmapped__ = True # id is the unique identifier of a draft variable. id: Mapped[str] = mapped_column(StringUUID, primary_key=True, server_default=db.text("uuid_generate_v4()")) @@ -885,14 +990,59 @@ class WorkflowDraftVariable(Base): selector: Mapped[str] = mapped_column(sa.String(255), nullable=False, name="selector") + # The data type of this variable's value value_type: Mapped[SegmentType] = mapped_column(EnumText(SegmentType, length=20)) - # JSON string + + # The variable's value serialized as a JSON string value: Mapped[str] = mapped_column(sa.Text, nullable=False, name="value") - # visible + # Controls whether the variable should be displayed in the variable inspection panel visible: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True) + + # Determines whether this variable can be modified by users editable: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False) + # The `node_execution_id` field identifies the workflow node execution that created this variable. + # It corresponds to the `id` field in the `WorkflowNodeExecutionModel` model. + # + # This field is not `None` for system variables and node variables, and is `None` + # for conversation variables. + node_execution_id: Mapped[str | None] = mapped_column( + StringUUID, + nullable=True, + default=None, + ) + + # Cache for deserialized value + # + # NOTE(QuantumGhost): This field serves two purposes: + # + # 1. Caches deserialized values to reduce repeated parsing costs + # 2. Allows modification of the deserialized value after retrieval, + # particularly important for `File`` variables which require database + # lookups to obtain storage_key and other metadata + # + # Use double underscore prefix for better encapsulation, + # making this attribute harder to access from outside the class. + __value: Segment | None + + def __init__(self, *args, **kwargs): + """ + The constructor of `WorkflowDraftVariable` is not intended for + direct use outside this file. Its solo purpose is setup private state + used by the model instance. + + Please use the factory methods + (`new_conversation_variable`, `new_sys_variable`, `new_node_variable`) + defined below to create instances of this class. + """ + super().__init__(*args, **kwargs) + self.__value = None + + @orm.reconstructor + def _init_on_load(self): + self.__value = None + def get_selector(self) -> list[str]: selector = json.loads(self.selector) if not isinstance(selector, list): @@ -907,15 +1057,92 @@ class WorkflowDraftVariable(Base): def _set_selector(self, value: list[str]): self.selector = json.dumps(value) - def get_value(self) -> Segment | None: - return build_segment(json.loads(self.value)) + def _loads_value(self) -> Segment: + value = json.loads(self.value) + return self.build_segment_with_type(self.value_type, value) + + @staticmethod + def rebuild_file_types(value: Any) -> Any: + # NOTE(QuantumGhost): Temporary workaround for structured data handling. + # By this point, `output` has been converted to dict by + # `WorkflowEntry.handle_special_values`, so we need to + # reconstruct File objects from their serialized form + # to maintain proper variable saving behavior. + # + # Ideally, we should work with structured data objects directly + # rather than their serialized forms. + # However, multiple components in the codebase depend on + # `WorkflowEntry.handle_special_values`, making a comprehensive migration challenging. + if isinstance(value, dict): + if not maybe_file_object(value): + return value + return File.model_validate(value) + elif isinstance(value, list) and value: + first = value[0] + if not maybe_file_object(first): + return value + return [File.model_validate(i) for i in value] + else: + return value + + @classmethod + def build_segment_with_type(cls, segment_type: SegmentType, value: Any) -> Segment: + # Extends `variable_factory.build_segment_with_type` functionality by + # reconstructing `FileSegment`` or `ArrayFileSegment`` objects from + # their serialized dictionary or list representations, respectively. + if segment_type == SegmentType.FILE: + if isinstance(value, File): + return build_segment_with_type(segment_type, value) + elif isinstance(value, dict): + file = cls.rebuild_file_types(value) + return build_segment_with_type(segment_type, file) + else: + raise TypeMismatchError(f"expected dict or File for FileSegment, got {type(value)}") + if segment_type == SegmentType.ARRAY_FILE: + if not isinstance(value, list): + raise TypeMismatchError(f"expected list for ArrayFileSegment, got {type(value)}") + file_list = cls.rebuild_file_types(value) + return build_segment_with_type(segment_type=segment_type, value=file_list) + + return build_segment_with_type(segment_type=segment_type, value=value) + + def get_value(self) -> Segment: + """Decode the serialized value into its corresponding `Segment` object. + + This method caches the result, so repeated calls will return the same + object instance without re-parsing the serialized data. + + If you need to modify the returned `Segment`, use `value.model_copy()` + to create a copy first to avoid affecting the cached instance. + + For more information about the caching mechanism, see the documentation + of the `__value` field. + + Returns: + Segment: The deserialized value as a Segment object. + """ + + if self.__value is not None: + return self.__value + value = self._loads_value() + self.__value = value + return value def set_name(self, name: str): self.name = name self._set_selector([self.node_id, name]) def set_value(self, value: Segment): - self.value = json.dumps(value.value) + """Updates the `value` and corresponding `value_type` fields in the database model. + + This method also stores the provided Segment object in the deserialized cache + without creating a copy, allowing for efficient value access. + + Args: + value: The Segment object to store as the variable's value. + """ + self.__value = value + self.value = json.dumps(value, cls=variable_utils.SegmentJSONEncoder) self.value_type = value.value_type def get_node_id(self) -> str | None: @@ -941,6 +1168,7 @@ class WorkflowDraftVariable(Base): node_id: str, name: str, value: Segment, + node_execution_id: str | None, description: str = "", ) -> "WorkflowDraftVariable": variable = WorkflowDraftVariable() @@ -952,6 +1180,7 @@ class WorkflowDraftVariable(Base): variable.name = name variable.set_value(value) variable._set_selector(list(variable_utils.to_selector(node_id, name))) + variable.node_execution_id = node_execution_id return variable @classmethod @@ -961,13 +1190,17 @@ class WorkflowDraftVariable(Base): app_id: str, name: str, value: Segment, + description: str = "", ) -> "WorkflowDraftVariable": variable = cls._new( app_id=app_id, node_id=CONVERSATION_VARIABLE_NODE_ID, name=name, value=value, + description=description, + node_execution_id=None, ) + variable.editable = True return variable @classmethod @@ -977,9 +1210,16 @@ class WorkflowDraftVariable(Base): app_id: str, name: str, value: Segment, + node_execution_id: str, editable: bool = False, ) -> "WorkflowDraftVariable": - variable = cls._new(app_id=app_id, node_id=SYSTEM_VARIABLE_NODE_ID, name=name, value=value) + variable = cls._new( + app_id=app_id, + node_id=SYSTEM_VARIABLE_NODE_ID, + name=name, + node_execution_id=node_execution_id, + value=value, + ) variable.editable = editable return variable @@ -991,11 +1231,19 @@ class WorkflowDraftVariable(Base): node_id: str, name: str, value: Segment, + node_execution_id: str, visible: bool = True, + editable: bool = True, ) -> "WorkflowDraftVariable": - variable = cls._new(app_id=app_id, node_id=node_id, name=name, value=value) + variable = cls._new( + app_id=app_id, + node_id=node_id, + name=name, + node_execution_id=node_execution_id, + value=value, + ) variable.visible = visible - variable.editable = True + variable.editable = editable return variable @property diff --git a/api/mypy.ini b/api/mypy.ini index 865be3c17d..6836b2602b 100644 --- a/api/mypy.ini +++ b/api/mypy.ini @@ -2,6 +2,8 @@ warn_return_any = True warn_unused_configs = True check_untyped_defs = True +cache_fine_grained = True +sqlite_cache = True exclude = (?x)( core/model_runtime/model_providers/ | tests/ @@ -16,4 +18,3 @@ ignore_missing_imports=True [mypy-flask_restful.inputs] ignore_missing_imports=True - diff --git a/api/pyproject.toml b/api/pyproject.toml index f497274da1..7f1efa671f 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "dify-api" -dynamic = ["version"] +version = "1.6.0" requires-python = ">=3.11,<3.13" dependencies = [ + "arize-phoenix-otel~=0.9.2", "authlib==1.3.1", "azure-identity==1.16.1", "beautifulsoup4==4.12.2", @@ -56,7 +57,6 @@ dependencies = [ "opentelemetry-sdk==1.27.0", "opentelemetry-semantic-conventions==0.48b0", "opentelemetry-util-http==0.48b0", - "pandas-stubs~=2.2.3.241009", "pandas[excel,output-formatting,performance]~=2.2.2", "pandoc~=2.4", "psycogreen~=1.0.2", @@ -82,6 +82,9 @@ dependencies = [ "weave~=0.51.0", "yarl~=1.18.3", "webvtt-py~=0.5.1", + "sseclient-py>=1.8.0", + "httpx-sse>=0.4.0", + "sendgrid~=6.12.3", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. @@ -104,8 +107,8 @@ dev = [ "dotenv-linter~=0.5.0", "faker~=32.1.0", "lxml-stubs~=0.5.1", - "mypy~=1.15.0", - "ruff~=0.11.5", + "mypy~=1.16.0", + "ruff~=0.12.3", "pytest~=8.3.2", "pytest-benchmark~=4.0.0", "pytest-cov~=4.1.0", @@ -149,9 +152,13 @@ dev = [ "types-ujson>=5.10.0", "boto3-stubs>=1.38.20", "types-jmespath>=1.0.2.20240106", + "hypothesis>=6.131.15", "types_pyOpenSSL>=24.1.0", "types_cffi>=1.17.0", "types_setuptools>=80.9.0", + "pandas-stubs~=2.2.3", + "scipy-stubs>=1.15.3.0", + "types-python-http-client>=3.3.7.20240910", ] ############################################################ @@ -194,11 +201,12 @@ vdb = [ "pymochow==1.3.1", "pyobvector~=0.1.6", "qdrant-client==1.9.0", - "tablestore==6.1.0", + "tablestore==6.2.0", "tcvectordb~=1.6.4", "tidb-vector==0.0.9", "upstash-vector==0.6.0", "volcengine-compat~=1.0.0", "weaviate-client~=3.24.0", "xinference-client~=1.2.2", + "mo-vector~=0.1.13", ] diff --git a/api/repositories/__init__.py b/api/repositories/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/repositories/api_workflow_node_execution_repository.py b/api/repositories/api_workflow_node_execution_repository.py new file mode 100644 index 0000000000..00a2d1f87d --- /dev/null +++ b/api/repositories/api_workflow_node_execution_repository.py @@ -0,0 +1,197 @@ +""" +Service-layer repository protocol for WorkflowNodeExecutionModel operations. + +This module provides a protocol interface for service-layer operations on WorkflowNodeExecutionModel +that abstracts database queries currently done directly in service classes. This repository is +specifically designed for service-layer needs and is separate from the core domain repository. + +The service repository handles operations that require access to database-specific fields like +tenant_id, app_id, triggered_from, etc., which are not part of the core domain model. +""" + +from collections.abc import Sequence +from datetime import datetime +from typing import Optional, Protocol + +from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from models.workflow import WorkflowNodeExecutionModel + + +class DifyAPIWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository, Protocol): + """ + Protocol for service-layer operations on WorkflowNodeExecutionModel. + + This repository provides database access patterns specifically needed by service classes, + handling queries that involve database-specific fields and multi-tenancy concerns. + + Key responsibilities: + - Manages database operations for workflow node executions + - Handles multi-tenant data isolation + - Provides batch processing capabilities + - Supports execution lifecycle management + + Implementation notes: + - Returns database models directly (WorkflowNodeExecutionModel) + - Handles tenant/app filtering automatically + - Provides service-specific query patterns + - Focuses on database operations without domain logic + - Supports cleanup and maintenance operations + """ + + def get_node_last_execution( + self, + tenant_id: str, + app_id: str, + workflow_id: str, + node_id: str, + ) -> Optional[WorkflowNodeExecutionModel]: + """ + Get the most recent execution for a specific node. + + This method finds the latest execution of a specific node within a workflow, + ordered by creation time. Used primarily for debugging and inspection purposes. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + workflow_id: The workflow identifier + node_id: The node identifier + + Returns: + The most recent WorkflowNodeExecutionModel for the node, or None if not found + """ + ... + + def get_executions_by_workflow_run( + self, + tenant_id: str, + app_id: str, + workflow_run_id: str, + ) -> Sequence[WorkflowNodeExecutionModel]: + """ + Get all node executions for a specific workflow run. + + This method retrieves all node executions that belong to a specific workflow run, + ordered by index in descending order for proper trace visualization. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + workflow_run_id: The workflow run identifier + + Returns: + A sequence of WorkflowNodeExecutionModel instances ordered by index (desc) + """ + ... + + def get_execution_by_id( + self, + execution_id: str, + tenant_id: Optional[str] = None, + ) -> Optional[WorkflowNodeExecutionModel]: + """ + Get a workflow node execution by its ID. + + This method retrieves a specific execution by its unique identifier. + Tenant filtering is optional for cases where the execution ID is globally unique. + + When `tenant_id` is None, it's the caller's responsibility to ensure proper data isolation between tenants. + If the `execution_id` comes from untrusted sources (e.g., retrieved from an API request), the caller should + set `tenant_id` to prevent horizontal privilege escalation. + + Args: + execution_id: The execution identifier + tenant_id: Optional tenant identifier for additional filtering + + Returns: + The WorkflowNodeExecutionModel if found, or None if not found + """ + ... + + def delete_expired_executions( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> int: + """ + Delete workflow node executions that are older than the specified date. + + This method is used for cleanup operations to remove expired executions + in batches to avoid overwhelming the database. + + Args: + tenant_id: The tenant identifier + before_date: Delete executions created before this date + batch_size: Maximum number of executions to delete in one batch + + Returns: + The number of executions deleted + """ + ... + + def delete_executions_by_app( + self, + tenant_id: str, + app_id: str, + batch_size: int = 1000, + ) -> int: + """ + Delete all workflow node executions for a specific app. + + This method is used when removing an app and all its related data. + Executions are deleted in batches to avoid overwhelming the database. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + batch_size: Maximum number of executions to delete in one batch + + Returns: + The total number of executions deleted + """ + ... + + def get_expired_executions_batch( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> Sequence[WorkflowNodeExecutionModel]: + """ + Get a batch of expired workflow node executions for backup purposes. + + This method retrieves expired executions without deleting them, + allowing the caller to backup the data before deletion. + + Args: + tenant_id: The tenant identifier + before_date: Get executions created before this date + batch_size: Maximum number of executions to retrieve + + Returns: + A sequence of WorkflowNodeExecutionModel instances + """ + ... + + def delete_executions_by_ids( + self, + execution_ids: Sequence[str], + ) -> int: + """ + Delete workflow node executions by their IDs. + + This method deletes specific executions by their IDs, + typically used after backing up the data. + + This method does not perform tenant isolation checks. The caller is responsible for ensuring proper + data isolation between tenants. When execution IDs come from untrusted sources (e.g., API requests), + additional tenant validation should be implemented to prevent unauthorized access. + + Args: + execution_ids: List of execution IDs to delete + + Returns: + The number of executions deleted + """ + ... diff --git a/api/repositories/api_workflow_run_repository.py b/api/repositories/api_workflow_run_repository.py new file mode 100644 index 0000000000..59e7baeb79 --- /dev/null +++ b/api/repositories/api_workflow_run_repository.py @@ -0,0 +1,181 @@ +""" +API WorkflowRun Repository Protocol + +This module defines the protocol for service-layer WorkflowRun operations. +The repository provides an abstraction layer for WorkflowRun database operations +used by service classes, separating service-layer concerns from core domain logic. + +Key Features: +- Paginated workflow run queries with filtering +- Bulk deletion operations with OSS backup support +- Multi-tenant data isolation +- Expired record cleanup with data retention +- Service-layer specific query patterns + +Usage: + This protocol should be used by service classes that need to perform + WorkflowRun database operations. It provides a clean interface that + hides implementation details and supports dependency injection. + +Example: + ```python + from repositories.dify_api_repository_factory import DifyAPIRepositoryFactory + + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + + # Get paginated workflow runs + runs = repo.get_paginated_workflow_runs( + tenant_id="tenant-123", + app_id="app-456", + triggered_from="debugging", + limit=20 + ) + ``` +""" + +from collections.abc import Sequence +from datetime import datetime +from typing import Optional, Protocol + +from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository +from libs.infinite_scroll_pagination import InfiniteScrollPagination +from models.workflow import WorkflowRun + + +class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol): + """ + Protocol for service-layer WorkflowRun repository operations. + + This protocol defines the interface for WorkflowRun database operations + that are specific to service-layer needs, including pagination, filtering, + and bulk operations with data backup support. + """ + + def get_paginated_workflow_runs( + self, + tenant_id: str, + app_id: str, + triggered_from: str, + limit: int = 20, + last_id: Optional[str] = None, + ) -> InfiniteScrollPagination: + """ + Get paginated workflow runs with filtering. + + Retrieves workflow runs for a specific app and trigger source with + cursor-based pagination support. Used primarily for debugging and + workflow run listing in the UI. + + Args: + tenant_id: Tenant identifier for multi-tenant isolation + app_id: Application identifier + triggered_from: Filter by trigger source (e.g., "debugging", "app-run") + limit: Maximum number of records to return (default: 20) + last_id: Cursor for pagination - ID of the last record from previous page + + Returns: + InfiniteScrollPagination object containing: + - data: List of WorkflowRun objects + - limit: Applied limit + - has_more: Boolean indicating if more records exist + + Raises: + ValueError: If last_id is provided but the corresponding record doesn't exist + """ + ... + + def get_workflow_run_by_id( + self, + tenant_id: str, + app_id: str, + run_id: str, + ) -> Optional[WorkflowRun]: + """ + Get a specific workflow run by ID. + + Retrieves a single workflow run with tenant and app isolation. + Used for workflow run detail views and execution tracking. + + Args: + tenant_id: Tenant identifier for multi-tenant isolation + app_id: Application identifier + run_id: Workflow run identifier + + Returns: + WorkflowRun object if found, None otherwise + """ + ... + + def get_expired_runs_batch( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> Sequence[WorkflowRun]: + """ + Get a batch of expired workflow runs for cleanup. + + Retrieves workflow runs created before the specified date for + cleanup operations. Used by scheduled tasks to remove old data + while maintaining data retention policies. + + Args: + tenant_id: Tenant identifier for multi-tenant isolation + before_date: Only return runs created before this date + batch_size: Maximum number of records to return + + Returns: + Sequence of WorkflowRun objects to be processed for cleanup + """ + ... + + def delete_runs_by_ids( + self, + run_ids: Sequence[str], + ) -> int: + """ + Delete workflow runs by their IDs. + + Performs bulk deletion of workflow runs by ID. This method should + be used after backing up the data to OSS storage for retention. + + Args: + run_ids: Sequence of workflow run IDs to delete + + Returns: + Number of records actually deleted + + Note: + This method performs hard deletion. Ensure data is backed up + to OSS storage before calling this method for compliance with + data retention policies. + """ + ... + + def delete_runs_by_app( + self, + tenant_id: str, + app_id: str, + batch_size: int = 1000, + ) -> int: + """ + Delete all workflow runs for a specific app. + + Performs bulk deletion of all workflow runs associated with an app. + Used during app cleanup operations. Processes records in batches + to avoid memory issues and long-running transactions. + + Args: + tenant_id: Tenant identifier for multi-tenant isolation + app_id: Application identifier + batch_size: Number of records to process in each batch + + Returns: + Total number of records deleted across all batches + + Note: + This method performs hard deletion without backup. Use with caution + and ensure proper data retention policies are followed. + """ + ... diff --git a/api/repositories/factory.py b/api/repositories/factory.py new file mode 100644 index 0000000000..0a0adbf2c2 --- /dev/null +++ b/api/repositories/factory.py @@ -0,0 +1,103 @@ +""" +DifyAPI Repository Factory for creating repository instances. + +This factory is specifically designed for DifyAPI repositories that handle +service-layer operations with dependency injection patterns. +""" + +import logging + +from sqlalchemy.orm import sessionmaker + +from configs import dify_config +from core.repositories import DifyCoreRepositoryFactory, RepositoryImportError +from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository +from repositories.api_workflow_run_repository import APIWorkflowRunRepository + +logger = logging.getLogger(__name__) + + +class DifyAPIRepositoryFactory(DifyCoreRepositoryFactory): + """ + Factory for creating DifyAPI repository instances based on configuration. + + This factory handles the creation of repositories that are specifically designed + for service-layer operations and use dependency injection with sessionmaker + for better testability and separation of concerns. + """ + + @classmethod + def create_api_workflow_node_execution_repository( + cls, session_maker: sessionmaker + ) -> DifyAPIWorkflowNodeExecutionRepository: + """ + Create a DifyAPIWorkflowNodeExecutionRepository instance based on configuration. + + This repository is designed for service-layer operations and uses dependency injection + with a sessionmaker for better testability and separation of concerns. It provides + database access patterns specifically needed by service classes, handling queries + that involve database-specific fields and multi-tenancy concerns. + + Args: + session_maker: SQLAlchemy sessionmaker to inject for database session management. + + Returns: + Configured DifyAPIWorkflowNodeExecutionRepository instance + + Raises: + RepositoryImportError: If the configured repository cannot be imported or instantiated + """ + class_path = dify_config.API_WORKFLOW_NODE_EXECUTION_REPOSITORY + logger.debug(f"Creating DifyAPIWorkflowNodeExecutionRepository from: {class_path}") + + try: + repository_class = cls._import_class(class_path) + cls._validate_repository_interface(repository_class, DifyAPIWorkflowNodeExecutionRepository) + # Service repository requires session_maker parameter + cls._validate_constructor_signature(repository_class, ["session_maker"]) + + return repository_class(session_maker=session_maker) # type: ignore[no-any-return] + except RepositoryImportError: + # Re-raise our custom errors as-is + raise + except Exception as e: + logger.exception("Failed to create DifyAPIWorkflowNodeExecutionRepository") + raise RepositoryImportError( + f"Failed to create DifyAPIWorkflowNodeExecutionRepository from '{class_path}': {e}" + ) from e + + @classmethod + def create_api_workflow_run_repository(cls, session_maker: sessionmaker) -> APIWorkflowRunRepository: + """ + Create an APIWorkflowRunRepository instance based on configuration. + + This repository is designed for service-layer WorkflowRun operations and uses dependency + injection with a sessionmaker for better testability and separation of concerns. It provides + database access patterns specifically needed by service classes for workflow run management, + including pagination, filtering, and bulk operations. + + Args: + session_maker: SQLAlchemy sessionmaker to inject for database session management. + + Returns: + Configured APIWorkflowRunRepository instance + + Raises: + RepositoryImportError: If the configured repository cannot be imported or instantiated + """ + class_path = dify_config.API_WORKFLOW_RUN_REPOSITORY + logger.debug(f"Creating APIWorkflowRunRepository from: {class_path}") + + try: + repository_class = cls._import_class(class_path) + cls._validate_repository_interface(repository_class, APIWorkflowRunRepository) + # Service repository requires session_maker parameter + cls._validate_constructor_signature(repository_class, ["session_maker"]) + + return repository_class(session_maker=session_maker) # type: ignore[no-any-return] + except RepositoryImportError: + # Re-raise our custom errors as-is + raise + except Exception as e: + logger.exception("Failed to create APIWorkflowRunRepository") + raise RepositoryImportError(f"Failed to create APIWorkflowRunRepository from '{class_path}': {e}") from e diff --git a/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py new file mode 100644 index 0000000000..e6a23ddf9f --- /dev/null +++ b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py @@ -0,0 +1,290 @@ +""" +SQLAlchemy implementation of WorkflowNodeExecutionServiceRepository. + +This module provides a concrete implementation of the service repository protocol +using SQLAlchemy 2.0 style queries for WorkflowNodeExecutionModel operations. +""" + +from collections.abc import Sequence +from datetime import datetime +from typing import Optional + +from sqlalchemy import delete, desc, select +from sqlalchemy.orm import Session, sessionmaker + +from models.workflow import WorkflowNodeExecutionModel +from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository + + +class DifyAPISQLAlchemyWorkflowNodeExecutionRepository(DifyAPIWorkflowNodeExecutionRepository): + """ + SQLAlchemy implementation of DifyAPIWorkflowNodeExecutionRepository. + + This repository provides service-layer database operations for WorkflowNodeExecutionModel + using SQLAlchemy 2.0 style queries. It implements the DifyAPIWorkflowNodeExecutionRepository + protocol with the following features: + + - Multi-tenancy data isolation through tenant_id filtering + - Direct database model operations without domain conversion + - Batch processing for efficient large-scale operations + - Optimized query patterns for common access patterns + - Dependency injection for better testability and maintainability + - Session management and transaction handling with proper cleanup + - Maintenance operations for data lifecycle management + - Thread-safe database operations using session-per-request pattern + """ + + def __init__(self, session_maker: sessionmaker[Session]): + """ + Initialize the repository with a sessionmaker. + + Args: + session_maker: SQLAlchemy sessionmaker for creating database sessions + """ + self._session_maker = session_maker + + def get_node_last_execution( + self, + tenant_id: str, + app_id: str, + workflow_id: str, + node_id: str, + ) -> Optional[WorkflowNodeExecutionModel]: + """ + Get the most recent execution for a specific node. + + This method replicates the query pattern from WorkflowService.get_node_last_run() + using SQLAlchemy 2.0 style syntax. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + workflow_id: The workflow identifier + node_id: The node identifier + + Returns: + The most recent WorkflowNodeExecutionModel for the node, or None if not found + """ + stmt = ( + select(WorkflowNodeExecutionModel) + .where( + WorkflowNodeExecutionModel.tenant_id == tenant_id, + WorkflowNodeExecutionModel.app_id == app_id, + WorkflowNodeExecutionModel.workflow_id == workflow_id, + WorkflowNodeExecutionModel.node_id == node_id, + ) + .order_by(desc(WorkflowNodeExecutionModel.created_at)) + .limit(1) + ) + + with self._session_maker() as session: + return session.scalar(stmt) + + def get_executions_by_workflow_run( + self, + tenant_id: str, + app_id: str, + workflow_run_id: str, + ) -> Sequence[WorkflowNodeExecutionModel]: + """ + Get all node executions for a specific workflow run. + + This method replicates the query pattern from WorkflowRunService.get_workflow_run_node_executions() + using SQLAlchemy 2.0 style syntax. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + workflow_run_id: The workflow run identifier + + Returns: + A sequence of WorkflowNodeExecutionModel instances ordered by index (desc) + """ + stmt = ( + select(WorkflowNodeExecutionModel) + .where( + WorkflowNodeExecutionModel.tenant_id == tenant_id, + WorkflowNodeExecutionModel.app_id == app_id, + WorkflowNodeExecutionModel.workflow_run_id == workflow_run_id, + ) + .order_by(desc(WorkflowNodeExecutionModel.index)) + ) + + with self._session_maker() as session: + return session.execute(stmt).scalars().all() + + def get_execution_by_id( + self, + execution_id: str, + tenant_id: Optional[str] = None, + ) -> Optional[WorkflowNodeExecutionModel]: + """ + Get a workflow node execution by its ID. + + This method replicates the query pattern from WorkflowDraftVariableService + and WorkflowService.single_step_run_workflow_node() using SQLAlchemy 2.0 style syntax. + + When `tenant_id` is None, it's the caller's responsibility to ensure proper data isolation between tenants. + If the `execution_id` comes from untrusted sources (e.g., retrieved from an API request), the caller should + set `tenant_id` to prevent horizontal privilege escalation. + + Args: + execution_id: The execution identifier + tenant_id: Optional tenant identifier for additional filtering + + Returns: + The WorkflowNodeExecutionModel if found, or None if not found + """ + stmt = select(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id == execution_id) + + # Add tenant filtering if provided + if tenant_id is not None: + stmt = stmt.where(WorkflowNodeExecutionModel.tenant_id == tenant_id) + + with self._session_maker() as session: + return session.scalar(stmt) + + def delete_expired_executions( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> int: + """ + Delete workflow node executions that are older than the specified date. + + Args: + tenant_id: The tenant identifier + before_date: Delete executions created before this date + batch_size: Maximum number of executions to delete in one batch + + Returns: + The number of executions deleted + """ + total_deleted = 0 + + while True: + with self._session_maker() as session: + # Find executions to delete in batches + stmt = ( + select(WorkflowNodeExecutionModel.id) + .where( + WorkflowNodeExecutionModel.tenant_id == tenant_id, + WorkflowNodeExecutionModel.created_at < before_date, + ) + .limit(batch_size) + ) + + execution_ids = session.execute(stmt).scalars().all() + if not execution_ids: + break + + # Delete the batch + delete_stmt = delete(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id.in_(execution_ids)) + result = session.execute(delete_stmt) + session.commit() + total_deleted += result.rowcount + + # If we deleted fewer than the batch size, we're done + if len(execution_ids) < batch_size: + break + + return total_deleted + + def delete_executions_by_app( + self, + tenant_id: str, + app_id: str, + batch_size: int = 1000, + ) -> int: + """ + Delete all workflow node executions for a specific app. + + Args: + tenant_id: The tenant identifier + app_id: The application identifier + batch_size: Maximum number of executions to delete in one batch + + Returns: + The total number of executions deleted + """ + total_deleted = 0 + + while True: + with self._session_maker() as session: + # Find executions to delete in batches + stmt = ( + select(WorkflowNodeExecutionModel.id) + .where( + WorkflowNodeExecutionModel.tenant_id == tenant_id, + WorkflowNodeExecutionModel.app_id == app_id, + ) + .limit(batch_size) + ) + + execution_ids = session.execute(stmt).scalars().all() + if not execution_ids: + break + + # Delete the batch + delete_stmt = delete(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id.in_(execution_ids)) + result = session.execute(delete_stmt) + session.commit() + total_deleted += result.rowcount + + # If we deleted fewer than the batch size, we're done + if len(execution_ids) < batch_size: + break + + return total_deleted + + def get_expired_executions_batch( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> Sequence[WorkflowNodeExecutionModel]: + """ + Get a batch of expired workflow node executions for backup purposes. + + Args: + tenant_id: The tenant identifier + before_date: Get executions created before this date + batch_size: Maximum number of executions to retrieve + + Returns: + A sequence of WorkflowNodeExecutionModel instances + """ + stmt = ( + select(WorkflowNodeExecutionModel) + .where( + WorkflowNodeExecutionModel.tenant_id == tenant_id, + WorkflowNodeExecutionModel.created_at < before_date, + ) + .limit(batch_size) + ) + + with self._session_maker() as session: + return session.execute(stmt).scalars().all() + + def delete_executions_by_ids( + self, + execution_ids: Sequence[str], + ) -> int: + """ + Delete workflow node executions by their IDs. + + Args: + execution_ids: List of execution IDs to delete + + Returns: + The number of executions deleted + """ + if not execution_ids: + return 0 + + with self._session_maker() as session: + stmt = delete(WorkflowNodeExecutionModel).where(WorkflowNodeExecutionModel.id.in_(execution_ids)) + result = session.execute(stmt) + session.commit() + return result.rowcount diff --git a/api/repositories/sqlalchemy_api_workflow_run_repository.py b/api/repositories/sqlalchemy_api_workflow_run_repository.py new file mode 100644 index 0000000000..ebd1d74b20 --- /dev/null +++ b/api/repositories/sqlalchemy_api_workflow_run_repository.py @@ -0,0 +1,203 @@ +""" +SQLAlchemy API WorkflowRun Repository Implementation + +This module provides the SQLAlchemy-based implementation of the APIWorkflowRunRepository +protocol. It handles service-layer WorkflowRun database operations using SQLAlchemy 2.0 +style queries with proper session management and multi-tenant data isolation. + +Key Features: +- SQLAlchemy 2.0 style queries for modern database operations +- Cursor-based pagination for efficient large dataset handling +- Bulk operations with batch processing for performance +- Multi-tenant data isolation and security +- Proper session management with dependency injection + +Implementation Notes: +- Uses sessionmaker for consistent session management +- Implements cursor-based pagination using created_at timestamps +- Provides efficient bulk deletion with batch processing +- Maintains data consistency with proper transaction handling +""" + +import logging +from collections.abc import Sequence +from datetime import datetime +from typing import Optional, cast + +from sqlalchemy import delete, select +from sqlalchemy.orm import Session, sessionmaker + +from libs.infinite_scroll_pagination import InfiniteScrollPagination +from models.workflow import WorkflowRun +from repositories.api_workflow_run_repository import APIWorkflowRunRepository + +logger = logging.getLogger(__name__) + + +class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository): + """ + SQLAlchemy implementation of APIWorkflowRunRepository. + + Provides service-layer WorkflowRun database operations using SQLAlchemy 2.0 + style queries. Supports dependency injection through sessionmaker and + maintains proper multi-tenant data isolation. + + Args: + session_maker: SQLAlchemy sessionmaker instance for database connections + """ + + def __init__(self, session_maker: sessionmaker[Session]) -> None: + """ + Initialize the repository with a sessionmaker. + + Args: + session_maker: SQLAlchemy sessionmaker for database connections + """ + self._session_maker = session_maker + + def get_paginated_workflow_runs( + self, + tenant_id: str, + app_id: str, + triggered_from: str, + limit: int = 20, + last_id: Optional[str] = None, + ) -> InfiniteScrollPagination: + """ + Get paginated workflow runs with filtering. + + Implements cursor-based pagination using created_at timestamps for + efficient handling of large datasets. Filters by tenant, app, and + trigger source for proper data isolation. + """ + with self._session_maker() as session: + # Build base query with filters + base_stmt = select(WorkflowRun).where( + WorkflowRun.tenant_id == tenant_id, + WorkflowRun.app_id == app_id, + WorkflowRun.triggered_from == triggered_from, + ) + + if last_id: + # Get the last workflow run for cursor-based pagination + last_run_stmt = base_stmt.where(WorkflowRun.id == last_id) + last_workflow_run = session.scalar(last_run_stmt) + + if not last_workflow_run: + raise ValueError("Last workflow run not exists") + + # Get records created before the last run's timestamp + base_stmt = base_stmt.where( + WorkflowRun.created_at < last_workflow_run.created_at, + WorkflowRun.id != last_workflow_run.id, + ) + + # First page - get most recent records + workflow_runs = session.scalars(base_stmt.order_by(WorkflowRun.created_at.desc()).limit(limit + 1)).all() + + # Check if there are more records for pagination + has_more = len(workflow_runs) > limit + if has_more: + workflow_runs = workflow_runs[:-1] + + return InfiniteScrollPagination(data=workflow_runs, limit=limit, has_more=has_more) + + def get_workflow_run_by_id( + self, + tenant_id: str, + app_id: str, + run_id: str, + ) -> Optional[WorkflowRun]: + """ + Get a specific workflow run by ID with tenant and app isolation. + """ + with self._session_maker() as session: + stmt = select(WorkflowRun).where( + WorkflowRun.tenant_id == tenant_id, + WorkflowRun.app_id == app_id, + WorkflowRun.id == run_id, + ) + return cast(Optional[WorkflowRun], session.scalar(stmt)) + + def get_expired_runs_batch( + self, + tenant_id: str, + before_date: datetime, + batch_size: int = 1000, + ) -> Sequence[WorkflowRun]: + """ + Get a batch of expired workflow runs for cleanup operations. + """ + with self._session_maker() as session: + stmt = ( + select(WorkflowRun) + .where( + WorkflowRun.tenant_id == tenant_id, + WorkflowRun.created_at < before_date, + ) + .limit(batch_size) + ) + return cast(Sequence[WorkflowRun], session.scalars(stmt).all()) + + def delete_runs_by_ids( + self, + run_ids: Sequence[str], + ) -> int: + """ + Delete workflow runs by their IDs using bulk deletion. + """ + if not run_ids: + return 0 + + with self._session_maker() as session: + stmt = delete(WorkflowRun).where(WorkflowRun.id.in_(run_ids)) + result = session.execute(stmt) + session.commit() + + deleted_count = cast(int, result.rowcount) + logger.info(f"Deleted {deleted_count} workflow runs by IDs") + return deleted_count + + def delete_runs_by_app( + self, + tenant_id: str, + app_id: str, + batch_size: int = 1000, + ) -> int: + """ + Delete all workflow runs for a specific app in batches. + """ + total_deleted = 0 + + while True: + with self._session_maker() as session: + # Get a batch of run IDs to delete + stmt = ( + select(WorkflowRun.id) + .where( + WorkflowRun.tenant_id == tenant_id, + WorkflowRun.app_id == app_id, + ) + .limit(batch_size) + ) + run_ids = session.scalars(stmt).all() + + if not run_ids: + break + + # Delete the batch + delete_stmt = delete(WorkflowRun).where(WorkflowRun.id.in_(run_ids)) + result = session.execute(delete_stmt) + session.commit() + + batch_deleted = result.rowcount + total_deleted += batch_deleted + + logger.info(f"Deleted batch of {batch_deleted} workflow runs for app {app_id}") + + # If we deleted fewer records than the batch size, we're done + if batch_deleted < batch_size: + break + + logger.info(f"Total deleted {total_deleted} workflow runs for app {app_id}") + return total_deleted diff --git a/api/services/account_service.py b/api/services/account_service.py index ac84a46299..2ba6f4345b 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -1,7 +1,6 @@ import base64 import json import logging -import random import secrets import uuid from datetime import UTC, datetime, timedelta @@ -17,7 +16,7 @@ from configs import dify_config from constants.languages import language_timezone_mapping, languages from events.tenant_event import tenant_was_created from extensions.ext_database import db -from extensions.ext_redis import redis_client +from extensions.ext_redis import redis_client, redis_fallback from libs.helper import RateLimiter, TokenManager from libs.passport import PassportService from libs.password import compare_password, hash_password, valid_password @@ -261,7 +260,7 @@ class AccountService: @staticmethod def generate_account_deletion_verification_code(account: Account) -> tuple[str, str]: - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( account=account, token_type="account_deletion", additional_data={"code": code} ) @@ -429,7 +428,7 @@ class AccountService: additional_data: dict[str, Any] = {}, ): if not code: - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) additional_data["code"] = code token = TokenManager.generate_token( account=account, email=email, token_type="reset_password", additional_data=additional_data @@ -456,7 +455,7 @@ class AccountService: raise EmailCodeLoginRateLimitExceededError() - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) @@ -496,6 +495,7 @@ class AccountService: return account @staticmethod + @redis_fallback(default_return=None) def add_login_error_rate_limit(email: str) -> None: key = f"login_error_rate_limit:{email}" count = redis_client.get(key) @@ -505,6 +505,7 @@ class AccountService: redis_client.setex(key, dify_config.LOGIN_LOCKOUT_DURATION, count) @staticmethod + @redis_fallback(default_return=False) def is_login_error_rate_limit(email: str) -> bool: key = f"login_error_rate_limit:{email}" count = redis_client.get(key) @@ -517,11 +518,13 @@ class AccountService: return False @staticmethod + @redis_fallback(default_return=None) def reset_login_error_rate_limit(email: str): key = f"login_error_rate_limit:{email}" redis_client.delete(key) @staticmethod + @redis_fallback(default_return=None) def add_forgot_password_error_rate_limit(email: str) -> None: key = f"forgot_password_error_rate_limit:{email}" count = redis_client.get(key) @@ -531,6 +534,7 @@ class AccountService: redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count) @staticmethod + @redis_fallback(default_return=False) def is_forgot_password_error_rate_limit(email: str) -> bool: key = f"forgot_password_error_rate_limit:{email}" count = redis_client.get(key) @@ -543,11 +547,13 @@ class AccountService: return False @staticmethod + @redis_fallback(default_return=None) def reset_forgot_password_error_rate_limit(email: str): key = f"forgot_password_error_rate_limit:{email}" redis_client.delete(key) @staticmethod + @redis_fallback(default_return=False) def is_email_send_ip_limit(ip_address: str): minute_key = f"email_send_ip_limit_minute:{ip_address}" freeze_key = f"email_send_ip_limit_freeze:{ip_address}" @@ -890,7 +896,7 @@ class RegisterService: TenantService.create_owner_tenant_if_not_exist(account=account, is_setup=True) - dify_setup = DifySetup(version=dify_config.CURRENT_VERSION) + dify_setup = DifySetup(version=dify_config.project.version) db.session.add(dify_setup) db.session.commit() except Exception as e: diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index d2875180d8..20257fa345 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -32,6 +32,7 @@ from models import Account, App, AppMode from models.model import AppModelConfig from models.workflow import Workflow from services.plugin.dependencies_analysis import DependenciesAnalysisService +from services.workflow_draft_variable_service import WorkflowDraftVariableService from services.workflow_service import WorkflowService logger = logging.getLogger(__name__) @@ -292,6 +293,8 @@ class AppDslService: dependencies=check_dependencies_pending_data, ) + draft_var_srv = WorkflowDraftVariableService(session=self._session) + draft_var_srv.delete_workflow_variables(app_id=app.id) return Import( id=import_id, status=status, @@ -421,7 +424,7 @@ class AppDslService: # Set icon type icon_type_value = icon_type or app_data.get("icon_type") - if icon_type_value in ["emoji", "link"]: + if icon_type_value in ["emoji", "link", "image"]: icon_type = icon_type_value else: icon_type = "emoji" diff --git a/api/services/app_service.py b/api/services/app_service.py index ebebf8fa58..db0f8cd414 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -47,8 +47,6 @@ class AppService: filters.append(App.mode == AppMode.ADVANCED_CHAT.value) elif args["mode"] == "agent-chat": filters.append(App.mode == AppMode.AGENT_CHAT.value) - elif args["mode"] == "channel": - filters.append(App.mode == AppMode.CHANNEL.value) if args.get("is_created_by_me", False): filters.append(App.created_by == user_id) @@ -395,3 +393,15 @@ class AppService: if not site: raise ValueError(f"App with id {app_id} not found") return str(site.code) + + @staticmethod + def get_app_id_by_code(app_code: str) -> str: + """ + Get app id by app code + :param app_code: app code + :return: app id + """ + site = db.session.query(Site).filter(Site.code == app_code).first() + if not site: + raise ValueError(f"App with code {app_code} not found") + return str(site.app_id) diff --git a/api/services/audio_service.py b/api/services/audio_service.py index a259f5a4c4..e8923eb51b 100644 --- a/api/services/audio_service.py +++ b/api/services/audio_service.py @@ -1,13 +1,17 @@ import io import logging import uuid +from collections.abc import Generator from typing import Optional +from flask import Response, stream_with_context from werkzeug.datastructures import FileStorage from constants import AUDIO_EXTENSIONS from core.model_manager import ModelManager from core.model_runtime.entities.model_entities import ModelType +from extensions.ext_database import db +from models.enums import MessageStatus from models.model import App, AppMode, AppModelConfig, Message from services.errors.audio import ( AudioTooLargeServiceError, @@ -16,6 +20,7 @@ from services.errors.audio import ( ProviderNotSupportTextToSpeechServiceError, UnsupportedAudioTypeServiceError, ) +from services.workflow_service import WorkflowService FILE_SIZE = 30 FILE_SIZE_LIMIT = FILE_SIZE * 1024 * 1024 @@ -74,35 +79,36 @@ class AudioService: voice: Optional[str] = None, end_user: Optional[str] = None, message_id: Optional[str] = None, + is_draft: bool = False, ): - from collections.abc import Generator - - from flask import Response, stream_with_context - from app import app - from extensions.ext_database import db - def invoke_tts(text_content: str, app_model: App, voice: Optional[str] = None): + def invoke_tts(text_content: str, app_model: App, voice: Optional[str] = None, is_draft: bool = False): with app.app_context(): - if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: - workflow = app_model.workflow - if workflow is None: - raise ValueError("TTS is not enabled") - - features_dict = workflow.features_dict - if "text_to_speech" not in features_dict or not features_dict["text_to_speech"].get("enabled"): - raise ValueError("TTS is not enabled") - - voice = features_dict["text_to_speech"].get("voice") if voice is None else voice - else: - if app_model.app_model_config is None: - raise ValueError("AppModelConfig not found") - text_to_speech_dict = app_model.app_model_config.text_to_speech_dict - - if not text_to_speech_dict.get("enabled"): - raise ValueError("TTS is not enabled") - - voice = text_to_speech_dict.get("voice") if voice is None else voice + if voice is None: + if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: + if is_draft: + workflow = WorkflowService().get_draft_workflow(app_model=app_model) + else: + workflow = app_model.workflow + if ( + workflow is None + or "text_to_speech" not in workflow.features_dict + or not workflow.features_dict["text_to_speech"].get("enabled") + ): + raise ValueError("TTS is not enabled") + + voice = workflow.features_dict["text_to_speech"].get("voice") + else: + if not is_draft: + if app_model.app_model_config is None: + raise ValueError("AppModelConfig not found") + text_to_speech_dict = app_model.app_model_config.text_to_speech_dict + + if not text_to_speech_dict.get("enabled"): + raise ValueError("TTS is not enabled") + + voice = text_to_speech_dict.get("voice") model_manager = ModelManager() model_instance = model_manager.get_default_model_instance( @@ -132,18 +138,18 @@ class AudioService: message = db.session.query(Message).filter(Message.id == message_id).first() if message is None: return None - if message.answer == "" and message.status == "normal": + if message.answer == "" and message.status == MessageStatus.NORMAL: return None else: - response = invoke_tts(message.answer, app_model=app_model, voice=voice) + response = invoke_tts(text_content=message.answer, app_model=app_model, voice=voice, is_draft=is_draft) if isinstance(response, Generator): return Response(stream_with_context(response), content_type="audio/mpeg") return response else: if text is None: raise ValueError("Text is required") - response = invoke_tts(text, app_model, voice) + response = invoke_tts(text_content=text, app_model=app_model, voice=voice, is_draft=is_draft) if isinstance(response, Generator): return Response(stream_with_context(response), content_type="audio/mpeg") return response diff --git a/api/services/clear_free_plan_tenant_expired_logs.py b/api/services/clear_free_plan_tenant_expired_logs.py index 1fd560d581..ddd16b2e0c 100644 --- a/api/services/clear_free_plan_tenant_expired_logs.py +++ b/api/services/clear_free_plan_tenant_expired_logs.py @@ -6,7 +6,7 @@ from concurrent.futures import ThreadPoolExecutor import click from flask import Flask, current_app -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from configs import dify_config from core.model_runtime.utils.encoders import jsonable_encoder @@ -14,7 +14,7 @@ from extensions.ext_database import db from extensions.ext_storage import storage from models.account import Tenant from models.model import App, Conversation, Message -from models.workflow import WorkflowNodeExecutionModel, WorkflowRun +from repositories.factory import DifyAPIRepositoryFactory from services.billing_service import BillingService logger = logging.getLogger(__name__) @@ -105,84 +105,99 @@ class ClearFreePlanTenantExpiredLogs: ) ) - while True: - with Session(db.engine).no_autoflush as session: - workflow_node_executions = ( - session.query(WorkflowNodeExecutionModel) - .filter( - WorkflowNodeExecutionModel.tenant_id == tenant_id, - WorkflowNodeExecutionModel.created_at - < datetime.datetime.now() - datetime.timedelta(days=days), - ) - .limit(batch) - .all() - ) + # Process expired workflow node executions with backup + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + node_execution_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository(session_maker) + before_date = datetime.datetime.now() - datetime.timedelta(days=days) + total_deleted = 0 - if len(workflow_node_executions) == 0: - break + while True: + # Get a batch of expired executions for backup + workflow_node_executions = node_execution_repo.get_expired_executions_batch( + tenant_id=tenant_id, + before_date=before_date, + batch_size=batch, + ) - # save workflow node executions - storage.save( - f"free_plan_tenant_expired_logs/" - f"{tenant_id}/workflow_node_executions/{datetime.datetime.now().strftime('%Y-%m-%d')}" - f"-{time.time()}.json", - json.dumps( - jsonable_encoder(workflow_node_executions), - ).encode("utf-8"), - ) + if len(workflow_node_executions) == 0: + break + + # Save workflow node executions to storage + storage.save( + f"free_plan_tenant_expired_logs/" + f"{tenant_id}/workflow_node_executions/{datetime.datetime.now().strftime('%Y-%m-%d')}" + f"-{time.time()}.json", + json.dumps( + jsonable_encoder(workflow_node_executions), + ).encode("utf-8"), + ) - workflow_node_execution_ids = [ - workflow_node_execution.id for workflow_node_execution in workflow_node_executions - ] + # Extract IDs for deletion + workflow_node_execution_ids = [ + workflow_node_execution.id for workflow_node_execution in workflow_node_executions + ] - # delete workflow node executions - session.query(WorkflowNodeExecutionModel).filter( - WorkflowNodeExecutionModel.id.in_(workflow_node_execution_ids), - ).delete(synchronize_session=False) - session.commit() + # Delete the backed up executions + deleted_count = node_execution_repo.delete_executions_by_ids(workflow_node_execution_ids) + total_deleted += deleted_count - click.echo( - click.style( - f"[{datetime.datetime.now()}] Processed {len(workflow_node_execution_ids)}" - f" workflow node executions for tenant {tenant_id}" - ) + click.echo( + click.style( + f"[{datetime.datetime.now()}] Processed {len(workflow_node_execution_ids)}" + f" workflow node executions for tenant {tenant_id}" ) + ) + + # If we got fewer than the batch size, we're done + if len(workflow_node_executions) < batch: + break + + # Process expired workflow runs with backup + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + before_date = datetime.datetime.now() - datetime.timedelta(days=days) + total_deleted = 0 while True: - with Session(db.engine).no_autoflush as session: - workflow_runs = ( - session.query(WorkflowRun) - .filter( - WorkflowRun.tenant_id == tenant_id, - WorkflowRun.created_at < datetime.datetime.now() - datetime.timedelta(days=days), - ) - .limit(batch) - .all() - ) + # Get a batch of expired workflow runs for backup + workflow_runs = workflow_run_repo.get_expired_runs_batch( + tenant_id=tenant_id, + before_date=before_date, + batch_size=batch, + ) - if len(workflow_runs) == 0: - break + if len(workflow_runs) == 0: + break + + # Save workflow runs to storage + storage.save( + f"free_plan_tenant_expired_logs/" + f"{tenant_id}/workflow_runs/{datetime.datetime.now().strftime('%Y-%m-%d')}" + f"-{time.time()}.json", + json.dumps( + jsonable_encoder( + [workflow_run.to_dict() for workflow_run in workflow_runs], + ), + ).encode("utf-8"), + ) - # save workflow runs + # Extract IDs for deletion + workflow_run_ids = [workflow_run.id for workflow_run in workflow_runs] - storage.save( - f"free_plan_tenant_expired_logs/" - f"{tenant_id}/workflow_runs/{datetime.datetime.now().strftime('%Y-%m-%d')}" - f"-{time.time()}.json", - json.dumps( - jsonable_encoder( - [workflow_run.to_dict() for workflow_run in workflow_runs], - ), - ).encode("utf-8"), - ) + # Delete the backed up workflow runs + deleted_count = workflow_run_repo.delete_runs_by_ids(workflow_run_ids) + total_deleted += deleted_count - workflow_run_ids = [workflow_run.id for workflow_run in workflow_runs] + click.echo( + click.style( + f"[{datetime.datetime.now()}] Processed {len(workflow_run_ids)}" + f" workflow runs for tenant {tenant_id}" + ) + ) - # delete workflow runs - session.query(WorkflowRun).filter( - WorkflowRun.id.in_(workflow_run_ids), - ).delete(synchronize_session=False) - session.commit() + # If we got fewer than the batch size, we're done + if len(workflow_runs) < batch: + break @classmethod def process(cls, days: int, batch: int, tenant_ids: list[str]): diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 4a5e9b3520..e42b5ace75 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -2,7 +2,7 @@ import copy import datetime import json import logging -import random +import secrets import time import uuid from collections import Counter @@ -59,6 +59,7 @@ from services.external_knowledge_service import ExternalDatasetService from services.feature_service import FeatureModel, FeatureService from services.tag_service import TagService from services.vector_service import VectorService +from tasks.add_document_to_index_task import add_document_to_index_task from tasks.batch_clean_document_task import batch_clean_document_task from tasks.clean_notion_document_task import clean_notion_document_task from tasks.deal_dataset_vector_index_task import deal_dataset_vector_index_task @@ -70,6 +71,7 @@ from tasks.document_indexing_update_task import document_indexing_update_task from tasks.duplicate_document_indexing_task import duplicate_document_indexing_task from tasks.enable_segments_to_index_task import enable_segments_to_index_task from tasks.recover_document_indexing_task import recover_document_indexing_task +from tasks.remove_document_from_index_task import remove_document_from_index_task from tasks.retry_document_indexing_task import retry_document_indexing_task from tasks.sync_website_document_indexing_task import sync_website_document_indexing_task @@ -276,176 +278,351 @@ class DatasetService: except ProviderTokenNotInitError as ex: raise ValueError(ex.description) + @staticmethod + def check_reranking_model_setting(tenant_id: str, reranking_model_provider: str, reranking_model: str): + try: + model_manager = ModelManager() + model_manager.get_model_instance( + tenant_id=tenant_id, + provider=reranking_model_provider, + model_type=ModelType.RERANK, + model=reranking_model, + ) + except LLMBadRequestError: + raise ValueError( + "No Rerank Model available. Please configure a valid provider in the Settings -> Model Provider." + ) + except ProviderTokenNotInitError as ex: + raise ValueError(ex.description) + @staticmethod def update_dataset(dataset_id, data, user): + """ + Update dataset configuration and settings. + + Args: + dataset_id: The unique identifier of the dataset to update + data: Dictionary containing the update data + user: The user performing the update operation + + Returns: + Dataset: The updated dataset object + + Raises: + ValueError: If dataset not found or validation fails + NoPermissionError: If user lacks permission to update the dataset + """ + # Retrieve and validate dataset existence dataset = DatasetService.get_dataset(dataset_id) if not dataset: raise ValueError("Dataset not found") + # Verify user has permission to update this dataset DatasetService.check_dataset_permission(dataset, user) + + # Handle external dataset updates if dataset.provider == "external": - external_retrieval_model = data.get("external_retrieval_model", None) - if external_retrieval_model: - dataset.retrieval_model = external_retrieval_model - dataset.name = data.get("name", dataset.name) - dataset.description = data.get("description", "") - permission = data.get("permission") - if permission: - dataset.permission = permission - external_knowledge_id = data.get("external_knowledge_id", None) - db.session.add(dataset) - if not external_knowledge_id: - raise ValueError("External knowledge id is required.") - external_knowledge_api_id = data.get("external_knowledge_api_id", None) - if not external_knowledge_api_id: - raise ValueError("External knowledge api id is required.") - - with Session(db.engine) as session: - external_knowledge_binding = ( - session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id).first() - ) + return DatasetService._update_external_dataset(dataset, data, user) + else: + return DatasetService._update_internal_dataset(dataset, data, user) - if not external_knowledge_binding: - raise ValueError("External knowledge binding not found.") + @staticmethod + def _update_external_dataset(dataset, data, user): + """ + Update external dataset configuration. + + Args: + dataset: The dataset object to update + data: Update data dictionary + user: User performing the update + + Returns: + Dataset: Updated dataset object + """ + # Update retrieval model if provided + external_retrieval_model = data.get("external_retrieval_model", None) + if external_retrieval_model: + dataset.retrieval_model = external_retrieval_model + + # Update basic dataset properties + dataset.name = data.get("name", dataset.name) + dataset.description = data.get("description", dataset.description) + + # Update permission if provided + permission = data.get("permission") + if permission: + dataset.permission = permission + + # Validate and update external knowledge configuration + external_knowledge_id = data.get("external_knowledge_id", None) + external_knowledge_api_id = data.get("external_knowledge_api_id", None) + + if not external_knowledge_id: + raise ValueError("External knowledge id is required.") + if not external_knowledge_api_id: + raise ValueError("External knowledge api id is required.") + # Update metadata fields + dataset.updated_by = user.id if user else None + dataset.updated_at = datetime.datetime.utcnow() + db.session.add(dataset) - if ( - external_knowledge_binding.external_knowledge_id != external_knowledge_id - or external_knowledge_binding.external_knowledge_api_id != external_knowledge_api_id - ): - external_knowledge_binding.external_knowledge_id = external_knowledge_id - external_knowledge_binding.external_knowledge_api_id = external_knowledge_api_id - db.session.add(external_knowledge_binding) - db.session.commit() - else: - data.pop("partial_member_list", None) - data.pop("external_knowledge_api_id", None) - data.pop("external_knowledge_id", None) - data.pop("external_retrieval_model", None) - filtered_data = {k: v for k, v in data.items() if v is not None or k == "description"} - action = None - if dataset.indexing_technique != data["indexing_technique"]: - # if update indexing_technique - if data["indexing_technique"] == "economy": - action = "remove" - filtered_data["embedding_model"] = None - filtered_data["embedding_model_provider"] = None - filtered_data["collection_binding_id"] = None - elif data["indexing_technique"] == "high_quality": - action = "add" - # get embedding model setting - try: - model_manager = ModelManager() - embedding_model = model_manager.get_model_instance( - tenant_id=current_user.current_tenant_id, - provider=data["embedding_model_provider"], - model_type=ModelType.TEXT_EMBEDDING, - model=data["embedding_model"], - ) - filtered_data["embedding_model"] = embedding_model.model - filtered_data["embedding_model_provider"] = embedding_model.provider - dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( - embedding_model.provider, embedding_model.model - ) - filtered_data["collection_binding_id"] = dataset_collection_binding.id - except LLMBadRequestError: - raise ValueError( - "No Embedding Model available. Please configure a valid provider " - "in the Settings -> Model Provider." - ) - except ProviderTokenNotInitError as ex: - raise ValueError(ex.description) - else: - # add default plugin id to both setting sets, to make sure the plugin model provider is consistent - # Skip embedding model checks if not provided in the update request - if ( - "embedding_model_provider" not in data - or "embedding_model" not in data - or not data.get("embedding_model_provider") - or not data.get("embedding_model") - ): - # If the dataset already has embedding model settings, use those - if dataset.embedding_model_provider and dataset.embedding_model: - # Keep existing values - filtered_data["embedding_model_provider"] = dataset.embedding_model_provider - filtered_data["embedding_model"] = dataset.embedding_model - # If collection_binding_id exists, keep it too - if dataset.collection_binding_id: - filtered_data["collection_binding_id"] = dataset.collection_binding_id - # Otherwise, don't try to update embedding model settings at all - # Remove these fields from filtered_data if they exist but are None/empty - if "embedding_model_provider" in filtered_data and not filtered_data["embedding_model_provider"]: - del filtered_data["embedding_model_provider"] - if "embedding_model" in filtered_data and not filtered_data["embedding_model"]: - del filtered_data["embedding_model"] - else: - skip_embedding_update = False - try: - # Handle existing model provider - plugin_model_provider = dataset.embedding_model_provider - plugin_model_provider_str = None - if plugin_model_provider: - plugin_model_provider_str = str(ModelProviderID(plugin_model_provider)) - - # Handle new model provider from request - new_plugin_model_provider = data["embedding_model_provider"] - new_plugin_model_provider_str = None - if new_plugin_model_provider: - new_plugin_model_provider_str = str(ModelProviderID(new_plugin_model_provider)) - - # Only update embedding model if both values are provided and different from current - if ( - plugin_model_provider_str != new_plugin_model_provider_str - or data["embedding_model"] != dataset.embedding_model - ): - action = "update" - model_manager = ModelManager() - try: - embedding_model = model_manager.get_model_instance( - tenant_id=current_user.current_tenant_id, - provider=data["embedding_model_provider"], - model_type=ModelType.TEXT_EMBEDDING, - model=data["embedding_model"], - ) - except ProviderTokenNotInitError: - # If we can't get the embedding model, skip updating it - # and keep the existing settings if available - if dataset.embedding_model_provider and dataset.embedding_model: - filtered_data["embedding_model_provider"] = dataset.embedding_model_provider - filtered_data["embedding_model"] = dataset.embedding_model - if dataset.collection_binding_id: - filtered_data["collection_binding_id"] = dataset.collection_binding_id - # Skip the rest of the embedding model update - skip_embedding_update = True - if not skip_embedding_update: - filtered_data["embedding_model"] = embedding_model.model - filtered_data["embedding_model_provider"] = embedding_model.provider - dataset_collection_binding = ( - DatasetCollectionBindingService.get_dataset_collection_binding( - embedding_model.provider, embedding_model.model - ) - ) - filtered_data["collection_binding_id"] = dataset_collection_binding.id - except LLMBadRequestError: - raise ValueError( - "No Embedding Model available. Please configure a valid provider " - "in the Settings -> Model Provider." - ) - except ProviderTokenNotInitError as ex: - raise ValueError(ex.description) + # Update external knowledge binding + DatasetService._update_external_knowledge_binding(dataset.id, external_knowledge_id, external_knowledge_api_id) + + # Commit changes to database + db.session.commit() + + return dataset - filtered_data["updated_by"] = user.id - filtered_data["updated_at"] = datetime.datetime.now() + @staticmethod + def _update_external_knowledge_binding(dataset_id, external_knowledge_id, external_knowledge_api_id): + """ + Update external knowledge binding configuration. + + Args: + dataset_id: Dataset identifier + external_knowledge_id: External knowledge identifier + external_knowledge_api_id: External knowledge API identifier + """ + with Session(db.engine) as session: + external_knowledge_binding = ( + session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id).first() + ) - # update Retrieval model - filtered_data["retrieval_model"] = data["retrieval_model"] + if not external_knowledge_binding: + raise ValueError("External knowledge binding not found.") - db.session.query(Dataset).filter_by(id=dataset_id).update(filtered_data) + # Update binding if values have changed + if ( + external_knowledge_binding.external_knowledge_id != external_knowledge_id + or external_knowledge_binding.external_knowledge_api_id != external_knowledge_api_id + ): + external_knowledge_binding.external_knowledge_id = external_knowledge_id + external_knowledge_binding.external_knowledge_api_id = external_knowledge_api_id + db.session.add(external_knowledge_binding) + + @staticmethod + def _update_internal_dataset(dataset, data, user): + """ + Update internal dataset configuration. + + Args: + dataset: The dataset object to update + data: Update data dictionary + user: User performing the update + + Returns: + Dataset: Updated dataset object + """ + # Remove external-specific fields from update data + data.pop("partial_member_list", None) + data.pop("external_knowledge_api_id", None) + data.pop("external_knowledge_id", None) + data.pop("external_retrieval_model", None) + + # Filter out None values except for description field + filtered_data = {k: v for k, v in data.items() if v is not None or k == "description"} + + # Handle indexing technique changes and embedding model updates + action = DatasetService._handle_indexing_technique_change(dataset, data, filtered_data) + + # Add metadata fields + filtered_data["updated_by"] = user.id + filtered_data["updated_at"] = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + # update Retrieval model + filtered_data["retrieval_model"] = data["retrieval_model"] + + # Update dataset in database + db.session.query(Dataset).filter_by(id=dataset.id).update(filtered_data) + db.session.commit() + + # Trigger vector index task if indexing technique changed + if action: + deal_dataset_vector_index_task.delay(dataset.id, action) - db.session.commit() - if action: - deal_dataset_vector_index_task.delay(dataset_id, action) return dataset + @staticmethod + def _handle_indexing_technique_change(dataset, data, filtered_data): + """ + Handle changes in indexing technique and configure embedding models accordingly. + + Args: + dataset: Current dataset object + data: Update data dictionary + filtered_data: Filtered update data + + Returns: + str: Action to perform ('add', 'remove', 'update', or None) + """ + if dataset.indexing_technique != data["indexing_technique"]: + if data["indexing_technique"] == "economy": + # Remove embedding model configuration for economy mode + filtered_data["embedding_model"] = None + filtered_data["embedding_model_provider"] = None + filtered_data["collection_binding_id"] = None + return "remove" + elif data["indexing_technique"] == "high_quality": + # Configure embedding model for high quality mode + DatasetService._configure_embedding_model_for_high_quality(data, filtered_data) + return "add" + else: + # Handle embedding model updates when indexing technique remains the same + return DatasetService._handle_embedding_model_update_when_technique_unchanged(dataset, data, filtered_data) + return None + + @staticmethod + def _configure_embedding_model_for_high_quality(data, filtered_data): + """ + Configure embedding model settings for high quality indexing. + + Args: + data: Update data dictionary + filtered_data: Filtered update data to modify + """ + try: + model_manager = ModelManager() + embedding_model = model_manager.get_model_instance( + tenant_id=current_user.current_tenant_id, + provider=data["embedding_model_provider"], + model_type=ModelType.TEXT_EMBEDDING, + model=data["embedding_model"], + ) + filtered_data["embedding_model"] = embedding_model.model + filtered_data["embedding_model_provider"] = embedding_model.provider + dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( + embedding_model.provider, embedding_model.model + ) + filtered_data["collection_binding_id"] = dataset_collection_binding.id + except LLMBadRequestError: + raise ValueError( + "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider." + ) + except ProviderTokenNotInitError as ex: + raise ValueError(ex.description) + + @staticmethod + def _handle_embedding_model_update_when_technique_unchanged(dataset, data, filtered_data): + """ + Handle embedding model updates when indexing technique remains the same. + + Args: + dataset: Current dataset object + data: Update data dictionary + filtered_data: Filtered update data to modify + + Returns: + str: Action to perform ('update' or None) + """ + # Skip embedding model checks if not provided in the update request + if ( + "embedding_model_provider" not in data + or "embedding_model" not in data + or not data.get("embedding_model_provider") + or not data.get("embedding_model") + ): + DatasetService._preserve_existing_embedding_settings(dataset, filtered_data) + return None + else: + return DatasetService._update_embedding_model_settings(dataset, data, filtered_data) + + @staticmethod + def _preserve_existing_embedding_settings(dataset, filtered_data): + """ + Preserve existing embedding model settings when not provided in update. + + Args: + dataset: Current dataset object + filtered_data: Filtered update data to modify + """ + # If the dataset already has embedding model settings, use those + if dataset.embedding_model_provider and dataset.embedding_model: + filtered_data["embedding_model_provider"] = dataset.embedding_model_provider + filtered_data["embedding_model"] = dataset.embedding_model + # If collection_binding_id exists, keep it too + if dataset.collection_binding_id: + filtered_data["collection_binding_id"] = dataset.collection_binding_id + # Otherwise, don't try to update embedding model settings at all + # Remove these fields from filtered_data if they exist but are None/empty + if "embedding_model_provider" in filtered_data and not filtered_data["embedding_model_provider"]: + del filtered_data["embedding_model_provider"] + if "embedding_model" in filtered_data and not filtered_data["embedding_model"]: + del filtered_data["embedding_model"] + + @staticmethod + def _update_embedding_model_settings(dataset, data, filtered_data): + """ + Update embedding model settings with new values. + + Args: + dataset: Current dataset object + data: Update data dictionary + filtered_data: Filtered update data to modify + + Returns: + str: Action to perform ('update' or None) + """ + try: + # Compare current and new model provider settings + current_provider_str = ( + str(ModelProviderID(dataset.embedding_model_provider)) if dataset.embedding_model_provider else None + ) + new_provider_str = ( + str(ModelProviderID(data["embedding_model_provider"])) if data["embedding_model_provider"] else None + ) + + # Only update if values are different + if current_provider_str != new_provider_str or data["embedding_model"] != dataset.embedding_model: + DatasetService._apply_new_embedding_settings(dataset, data, filtered_data) + return "update" + except LLMBadRequestError: + raise ValueError( + "No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider." + ) + except ProviderTokenNotInitError as ex: + raise ValueError(ex.description) + return None + + @staticmethod + def _apply_new_embedding_settings(dataset, data, filtered_data): + """ + Apply new embedding model settings to the dataset. + + Args: + dataset: Current dataset object + data: Update data dictionary + filtered_data: Filtered update data to modify + """ + model_manager = ModelManager() + try: + embedding_model = model_manager.get_model_instance( + tenant_id=current_user.current_tenant_id, + provider=data["embedding_model_provider"], + model_type=ModelType.TEXT_EMBEDDING, + model=data["embedding_model"], + ) + except ProviderTokenNotInitError: + # If we can't get the embedding model, preserve existing settings + logging.warning( + f"Failed to initialize embedding model {data['embedding_model_provider']}/{data['embedding_model']}, " + f"preserving existing settings" + ) + if dataset.embedding_model_provider and dataset.embedding_model: + filtered_data["embedding_model_provider"] = dataset.embedding_model_provider + filtered_data["embedding_model"] = dataset.embedding_model + if dataset.collection_binding_id: + filtered_data["collection_binding_id"] = dataset.collection_binding_id + # Skip the rest of the embedding model update + return + + # Apply new embedding model settings + filtered_data["embedding_model"] = embedding_model.model + filtered_data["embedding_model_provider"] = embedding_model.provider + dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( + embedding_model.provider, embedding_model.model + ) + filtered_data["collection_binding_id"] = dataset_collection_binding.id + @staticmethod def delete_dataset(dataset_id, user): dataset = DatasetService.get_dataset(dataset_id) @@ -970,18 +1147,23 @@ class DocumentService: documents.append(document) batch = document.batch else: - batch = time.strftime("%Y%m%d%H%M%S") + str(random.randint(100000, 999999)) + batch = time.strftime("%Y%m%d%H%M%S") + str(100000 + secrets.randbelow(exclusive_upper_bound=900000)) # save process rule if not dataset_process_rule: process_rule = knowledge_config.process_rule if process_rule: if process_rule.mode in ("custom", "hierarchical"): - dataset_process_rule = DatasetProcessRule( - dataset_id=dataset.id, - mode=process_rule.mode, - rules=process_rule.rules.model_dump_json() if process_rule.rules else None, - created_by=account.id, - ) + if process_rule.rules: + dataset_process_rule = DatasetProcessRule( + dataset_id=dataset.id, + mode=process_rule.mode, + rules=process_rule.rules.model_dump_json() if process_rule.rules else None, + created_by=account.id, + ) + else: + dataset_process_rule = dataset.latest_process_rule + if not dataset_process_rule: + raise ValueError("No process rule found.") elif process_rule.mode == "automatic": dataset_process_rule = DatasetProcessRule( dataset_id=dataset.id, @@ -1402,16 +1584,16 @@ class DocumentService: knowledge_config.embedding_model, # type: ignore ) dataset_collection_binding_id = dataset_collection_binding.id - if knowledge_config.retrieval_model: - retrieval_model = knowledge_config.retrieval_model - else: - retrieval_model = RetrievalModel( - search_method=RetrievalMethod.SEMANTIC_SEARCH.value, - reranking_enable=False, - reranking_model=RerankingModel(reranking_provider_name="", reranking_model_name=""), - top_k=2, - score_threshold_enabled=False, - ) + if knowledge_config.retrieval_model: + retrieval_model = knowledge_config.retrieval_model + else: + retrieval_model = RetrievalModel( + search_method=RetrievalMethod.SEMANTIC_SEARCH.value, + reranking_enable=False, + reranking_model=RerankingModel(reranking_provider_name="", reranking_model_name=""), + top_k=2, + score_threshold_enabled=False, + ) # save dataset dataset = Dataset( tenant_id=tenant_id, @@ -1603,6 +1785,191 @@ class DocumentService: if not isinstance(args["process_rule"]["rules"]["segmentation"]["max_tokens"], int): raise ValueError("Process rule segmentation max_tokens is invalid") + @staticmethod + def batch_update_document_status(dataset: Dataset, document_ids: list[str], action: str, user): + """ + Batch update document status. + + Args: + dataset (Dataset): The dataset object + document_ids (list[str]): List of document IDs to update + action (str): Action to perform (enable, disable, archive, un_archive) + user: Current user performing the action + + Raises: + DocumentIndexingError: If document is being indexed or not in correct state + ValueError: If action is invalid + """ + if not document_ids: + return + + # Early validation of action parameter + valid_actions = ["enable", "disable", "archive", "un_archive"] + if action not in valid_actions: + raise ValueError(f"Invalid action: {action}. Must be one of {valid_actions}") + + documents_to_update = [] + + # First pass: validate all documents and prepare updates + for document_id in document_ids: + document = DocumentService.get_document(dataset.id, document_id) + if not document: + continue + + # Check if document is being indexed + indexing_cache_key = f"document_{document.id}_indexing" + cache_result = redis_client.get(indexing_cache_key) + if cache_result is not None: + raise DocumentIndexingError(f"Document:{document.name} is being indexed, please try again later") + + # Prepare update based on action + update_info = DocumentService._prepare_document_status_update(document, action, user) + if update_info: + documents_to_update.append(update_info) + + # Second pass: apply all updates in a single transaction + if documents_to_update: + try: + for update_info in documents_to_update: + document = update_info["document"] + updates = update_info["updates"] + + # Apply updates to the document + for field, value in updates.items(): + setattr(document, field, value) + + db.session.add(document) + + # Batch commit all changes + db.session.commit() + except Exception as e: + # Rollback on any error + db.session.rollback() + raise e + # Execute async tasks and set Redis cache after successful commit + # propagation_error is used to capture any errors for submitting async task execution + propagation_error = None + for update_info in documents_to_update: + try: + # Execute async tasks after successful commit + if update_info["async_task"]: + task_info = update_info["async_task"] + task_func = task_info["function"] + task_args = task_info["args"] + task_func.delay(*task_args) + except Exception as e: + # Log the error but do not rollback the transaction + logging.exception(f"Error executing async task for document {update_info['document'].id}") + # don't raise the error immediately, but capture it for later + propagation_error = e + try: + # Set Redis cache if needed after successful commit + if update_info["set_cache"]: + document = update_info["document"] + indexing_cache_key = f"document_{document.id}_indexing" + redis_client.setex(indexing_cache_key, 600, 1) + except Exception as e: + # Log the error but do not rollback the transaction + logging.exception(f"Error setting cache for document {update_info['document'].id}") + # Raise any propagation error after all updates + if propagation_error: + raise propagation_error + + @staticmethod + def _prepare_document_status_update(document, action: str, user): + """ + Prepare document status update information. + + Args: + document: Document object to update + action: Action to perform + user: Current user + + Returns: + dict: Update information or None if no update needed + """ + now = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + + if action == "enable": + return DocumentService._prepare_enable_update(document, now) + elif action == "disable": + return DocumentService._prepare_disable_update(document, user, now) + elif action == "archive": + return DocumentService._prepare_archive_update(document, user, now) + elif action == "un_archive": + return DocumentService._prepare_unarchive_update(document, now) + + return None + + @staticmethod + def _prepare_enable_update(document, now): + """Prepare updates for enabling a document.""" + if document.enabled: + return None + + return { + "document": document, + "updates": {"enabled": True, "disabled_at": None, "disabled_by": None, "updated_at": now}, + "async_task": {"function": add_document_to_index_task, "args": [document.id]}, + "set_cache": True, + } + + @staticmethod + def _prepare_disable_update(document, user, now): + """Prepare updates for disabling a document.""" + if not document.completed_at or document.indexing_status != "completed": + raise DocumentIndexingError(f"Document: {document.name} is not completed.") + + if not document.enabled: + return None + + return { + "document": document, + "updates": {"enabled": False, "disabled_at": now, "disabled_by": user.id, "updated_at": now}, + "async_task": {"function": remove_document_from_index_task, "args": [document.id]}, + "set_cache": True, + } + + @staticmethod + def _prepare_archive_update(document, user, now): + """Prepare updates for archiving a document.""" + if document.archived: + return None + + update_info = { + "document": document, + "updates": {"archived": True, "archived_at": now, "archived_by": user.id, "updated_at": now}, + "async_task": None, + "set_cache": False, + } + + # Only set async task and cache if document is currently enabled + if document.enabled: + update_info["async_task"] = {"function": remove_document_from_index_task, "args": [document.id]} + update_info["set_cache"] = True + + return update_info + + @staticmethod + def _prepare_unarchive_update(document, now): + """Prepare updates for unarchiving a document.""" + if not document.archived: + return None + + update_info = { + "document": document, + "updates": {"archived": False, "archived_at": None, "archived_by": None, "updated_at": now}, + "async_task": None, + "set_cache": False, + } + + # Only re-index if the document is currently enabled + if document.enabled: + update_info["async_task"] = {"function": add_document_to_index_task, "args": [document.id]} + update_info["set_cache"] = True + + return update_info + class SegmentService: @classmethod @@ -1857,6 +2224,7 @@ class SegmentService: # calc embedding use tokens if document.doc_form == "qa_model": + segment.answer = args.answer tokens = embedding_model.get_text_embedding_num_tokens(texts=[content + segment.answer])[0] else: tokens = embedding_model.get_text_embedding_num_tokens(texts=[content])[0] diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index 1be78d2e62..54d45f45ea 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -1,3 +1,5 @@ +from datetime import datetime + from pydantic import BaseModel, Field from services.enterprise.base import EnterpriseRequest @@ -5,7 +7,7 @@ from services.enterprise.base import EnterpriseRequest class WebAppSettings(BaseModel): access_mode: str = Field( - description="Access mode for the web app. Can be 'public' or 'private'", + description="Access mode for the web app. Can be 'public', 'private', 'private_all', 'sso_verified'", default="private", alias="accessMode", ) @@ -20,6 +22,28 @@ class EnterpriseService: def get_workspace_info(cls, tenant_id: str): return EnterpriseRequest.send_request("GET", f"/workspace/{tenant_id}/info") + @classmethod + def get_app_sso_settings_last_update_time(cls) -> datetime: + data = EnterpriseRequest.send_request("GET", "/sso/app/last-update-time") + if not data: + raise ValueError("No data found.") + try: + # parse the UTC timestamp from the response + return datetime.fromisoformat(data) + except ValueError as e: + raise ValueError(f"Invalid date format: {data}") from e + + @classmethod + def get_workspace_sso_settings_last_update_time(cls) -> datetime: + data = EnterpriseRequest.send_request("GET", "/sso/workspace/last-update-time") + if not data: + raise ValueError("No data found.") + try: + # parse the UTC timestamp from the response + return datetime.fromisoformat(data) + except ValueError as e: + raise ValueError(f"Invalid date format: {data}") from e + class WebAppAuth: @classmethod def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str): diff --git a/api/services/entities/knowledge_entities/knowledge_entities.py b/api/services/entities/knowledge_entities/knowledge_entities.py index bb3be61f85..88d4224e97 100644 --- a/api/services/entities/knowledge_entities/knowledge_entities.py +++ b/api/services/entities/knowledge_entities/knowledge_entities.py @@ -95,13 +95,13 @@ class WeightKeywordSetting(BaseModel): class WeightModel(BaseModel): - weight_type: Optional[str] = None + weight_type: Optional[Literal["semantic_first", "keyword_first", "customized"]] = None vector_setting: Optional[WeightVectorSetting] = None keyword_setting: Optional[WeightKeywordSetting] = None class RetrievalModel(BaseModel): - search_method: Literal["hybrid_search", "semantic_search", "full_text_search"] + search_method: Literal["hybrid_search", "semantic_search", "full_text_search", "keyword_search"] reranking_enable: bool reranking_model: Optional[RerankingModel] = None reranking_mode: Optional[str] = None diff --git a/api/services/errors/__init__.py b/api/services/errors/__init__.py index eb1f055708..697e691224 100644 --- a/api/services/errors/__init__.py +++ b/api/services/errors/__init__.py @@ -4,7 +4,6 @@ from . import ( app_model_config, audio, base, - completion, conversation, dataset, document, @@ -19,7 +18,6 @@ __all__ = [ "app_model_config", "audio", "base", - "completion", "conversation", "dataset", "document", diff --git a/api/services/errors/account.py b/api/services/errors/account.py index 5aca12ffeb..4d3d150e07 100644 --- a/api/services/errors/account.py +++ b/api/services/errors/account.py @@ -55,7 +55,3 @@ class MemberNotInTenantError(BaseServiceError): class RoleAlreadyAssignedError(BaseServiceError): pass - - -class RateLimitExceededError(BaseServiceError): - pass diff --git a/api/services/errors/app.py b/api/services/errors/app.py index 87e9e9247d..5d348c61be 100644 --- a/api/services/errors/app.py +++ b/api/services/errors/app.py @@ -4,3 +4,7 @@ class MoreLikeThisDisabledError(Exception): class WorkflowHashNotEqualError(Exception): pass + + +class IsDraftWorkflowError(Exception): + pass diff --git a/api/services/errors/completion.py b/api/services/errors/plugin.py similarity index 51% rename from api/services/errors/completion.py rename to api/services/errors/plugin.py index 7fc50a588e..be5b144b3d 100644 --- a/api/services/errors/completion.py +++ b/api/services/errors/plugin.py @@ -1,5 +1,5 @@ from services.errors.base import BaseServiceError -class CompletionStoppedError(BaseServiceError): +class PluginInstallationForbiddenError(BaseServiceError): pass diff --git a/api/services/feature_service.py b/api/services/feature_service.py index be85a03e80..188caf3505 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -88,6 +88,26 @@ class WebAppAuthModel(BaseModel): allow_email_password_login: bool = False +class PluginInstallationScope(StrEnum): + NONE = "none" + OFFICIAL_ONLY = "official_only" + OFFICIAL_AND_SPECIFIC_PARTNERS = "official_and_specific_partners" + ALL = "all" + + +class PluginInstallationPermissionModel(BaseModel): + # Plugin installation scope – possible values: + # none: prohibit all plugin installations + # official_only: allow only Dify official plugins + # official_and_specific_partners: allow official and specific partner plugins + # all: allow installation of all plugins + plugin_installation_scope: PluginInstallationScope = PluginInstallationScope.ALL + + # If True, restrict plugin installation to the marketplace only + # Equivalent to ForceEnablePluginVerification + restrict_to_marketplace_only: bool = False + + class FeatureModel(BaseModel): billing: BillingModel = BillingModel() education: EducationModel = EducationModel() @@ -128,6 +148,7 @@ class SystemFeatureModel(BaseModel): license: LicenseModel = LicenseModel() branding: BrandingModel = BrandingModel() webapp_auth: WebAppAuthModel = WebAppAuthModel() + plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() class FeatureService: @@ -291,3 +312,12 @@ class FeatureService: features.license.workspaces.enabled = license_info["workspaces"]["enabled"] features.license.workspaces.limit = license_info["workspaces"]["limit"] features.license.workspaces.size = license_info["workspaces"]["used"] + + if "PluginInstallationPermission" in enterprise_info: + plugin_installation_info = enterprise_info["PluginInstallationPermission"] + features.plugin_installation_permission.plugin_installation_scope = plugin_installation_info[ + "pluginInstallationScope" + ] + features.plugin_installation_permission.restrict_to_marketplace_only = plugin_installation_info[ + "restrictToMarketplaceOnly" + ] diff --git a/api/services/file_service.py b/api/services/file_service.py index 2d68f30c5a..286535bd18 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -18,6 +18,7 @@ from core.file import helpers as file_helpers from core.rag.extractor.extract_processor import ExtractProcessor from extensions.ext_database import db from extensions.ext_storage import storage +from libs.helper import extract_tenant_id from models.account import Account from models.enums import CreatorUserRole from models.model import EndUser, UploadFile @@ -61,11 +62,7 @@ class FileService: # generate file key file_uuid = str(uuid.uuid4()) - if isinstance(user, Account): - current_tenant_id = user.current_tenant_id - else: - # end_user - current_tenant_id = user.tenant_id + current_tenant_id = extract_tenant_id(user) file_key = "upload_files/" + (current_tenant_id or "") + "/" + file_uuid + "." + extension diff --git a/api/services/metadata_service.py b/api/services/metadata_service.py index 26d6d4ce18..cfcb121153 100644 --- a/api/services/metadata_service.py +++ b/api/services/metadata_service.py @@ -19,6 +19,10 @@ from services.entities.knowledge_entities.knowledge_entities import ( class MetadataService: @staticmethod def create_metadata(dataset_id: str, metadata_args: MetadataArgs) -> DatasetMetadata: + # check if metadata name is too long + if len(metadata_args.name) > 255: + raise ValueError("Metadata name cannot exceed 255 characters.") + # check if metadata name already exists if ( db.session.query(DatasetMetadata) @@ -42,6 +46,10 @@ class MetadataService: @staticmethod def update_metadata_name(dataset_id: str, metadata_id: str, name: str) -> DatasetMetadata: # type: ignore + # check if metadata name is too long + if len(name) > 255: + raise ValueError("Metadata name cannot exceed 255 characters.") + lock_key = f"dataset_metadata_lock_{dataset_id}" # check if metadata name already exists if ( diff --git a/api/services/moderation_service.py b/api/services/moderation_service.py deleted file mode 100644 index 082afeed89..0000000000 --- a/api/services/moderation_service.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Optional - -from core.moderation.factory import ModerationFactory, ModerationOutputsResult -from extensions.ext_database import db -from models.model import App, AppModelConfig - - -class ModerationService: - def moderation_for_outputs(self, app_id: str, app_model: App, text: str) -> ModerationOutputsResult: - app_model_config: Optional[AppModelConfig] = None - - app_model_config = ( - db.session.query(AppModelConfig).filter(AppModelConfig.id == app_model.app_model_config_id).first() - ) - - if not app_model_config: - raise ValueError("app model config not found") - - name = app_model_config.sensitive_word_avoidance_dict["type"] - config = app_model_config.sensitive_word_avoidance_dict["config"] - - moderation = ModerationFactory(name, app_id, app_model.tenant_id, config) - return moderation.moderation_for_outputs(text) diff --git a/api/services/ops_service.py b/api/services/ops_service.py index 792f50703e..dbeb4f1908 100644 --- a/api/services/ops_service.py +++ b/api/services/ops_service.py @@ -34,6 +34,24 @@ class OpsService: ) new_decrypt_tracing_config = OpsTraceManager.obfuscated_decrypt_token(tracing_provider, decrypt_tracing_config) + if tracing_provider == "arize" and ( + "project_url" not in decrypt_tracing_config or not decrypt_tracing_config.get("project_url") + ): + try: + project_url = OpsTraceManager.get_trace_config_project_url(decrypt_tracing_config, tracing_provider) + new_decrypt_tracing_config.update({"project_url": project_url}) + except Exception: + new_decrypt_tracing_config.update({"project_url": "https://app.arize.com/"}) + + if tracing_provider == "phoenix" and ( + "project_url" not in decrypt_tracing_config or not decrypt_tracing_config.get("project_url") + ): + try: + project_url = OpsTraceManager.get_trace_config_project_url(decrypt_tracing_config, tracing_provider) + new_decrypt_tracing_config.update({"project_url": project_url}) + except Exception: + new_decrypt_tracing_config.update({"project_url": "https://app.phoenix.arize.com/projects/"}) + if tracing_provider == "langfuse" and ( "project_key" not in decrypt_tracing_config or not decrypt_tracing_config.get("project_key") ): @@ -76,6 +94,16 @@ class OpsService: new_decrypt_tracing_config.update({"project_url": project_url}) except Exception: new_decrypt_tracing_config.update({"project_url": "https://wandb.ai/"}) + + if tracing_provider == "aliyun" and ( + "project_url" not in decrypt_tracing_config or not decrypt_tracing_config.get("project_url") + ): + try: + project_url = OpsTraceManager.get_trace_config_project_url(decrypt_tracing_config, tracing_provider) + new_decrypt_tracing_config.update({"project_url": project_url}) + except Exception: + new_decrypt_tracing_config.update({"project_url": "https://arms.console.aliyun.com/"}) + trace_config_data.tracing_config = new_decrypt_tracing_config return trace_config_data.to_dict() @@ -107,7 +135,9 @@ class OpsService: return {"error": "Invalid Credentials"} # get project url - if tracing_provider == "langfuse": + if tracing_provider in ("arize", "phoenix"): + project_url = OpsTraceManager.get_trace_config_project_url(tracing_config, tracing_provider) + elif tracing_provider == "langfuse": project_key = OpsTraceManager.get_trace_config_project_key(tracing_config, tracing_provider) project_url = "{host}/project/{key}".format(host=tracing_config.get("host"), key=project_key) elif tracing_provider in ("langsmith", "opik"): diff --git a/api/services/plugin/data_migration.py b/api/services/plugin/data_migration.py index 1c5abfecba..5324036414 100644 --- a/api/services/plugin/data_migration.py +++ b/api/services/plugin/data_migration.py @@ -3,7 +3,7 @@ import logging import click -from core.entities import DEFAULT_PLUGIN_ID +from core.plugin.entities.plugin import GenericProviderID, ModelProviderID, ToolProviderID from models.engine import db logger = logging.getLogger(__name__) @@ -12,17 +12,17 @@ logger = logging.getLogger(__name__) class PluginDataMigration: @classmethod def migrate(cls) -> None: - cls.migrate_db_records("providers", "provider_name") # large table - cls.migrate_db_records("provider_models", "provider_name") - cls.migrate_db_records("provider_orders", "provider_name") - cls.migrate_db_records("tenant_default_models", "provider_name") - cls.migrate_db_records("tenant_preferred_model_providers", "provider_name") - cls.migrate_db_records("provider_model_settings", "provider_name") - cls.migrate_db_records("load_balancing_model_configs", "provider_name") + cls.migrate_db_records("providers", "provider_name", ModelProviderID) # large table + cls.migrate_db_records("provider_models", "provider_name", ModelProviderID) + cls.migrate_db_records("provider_orders", "provider_name", ModelProviderID) + cls.migrate_db_records("tenant_default_models", "provider_name", ModelProviderID) + cls.migrate_db_records("tenant_preferred_model_providers", "provider_name", ModelProviderID) + cls.migrate_db_records("provider_model_settings", "provider_name", ModelProviderID) + cls.migrate_db_records("load_balancing_model_configs", "provider_name", ModelProviderID) cls.migrate_datasets() - cls.migrate_db_records("embeddings", "provider_name") # large table - cls.migrate_db_records("dataset_collection_bindings", "provider_name") - cls.migrate_db_records("tool_builtin_providers", "provider") + cls.migrate_db_records("embeddings", "provider_name", ModelProviderID) # large table + cls.migrate_db_records("dataset_collection_bindings", "provider_name", ModelProviderID) + cls.migrate_db_records("tool_builtin_providers", "provider", ToolProviderID) @classmethod def migrate_datasets(cls) -> None: @@ -66,9 +66,10 @@ limit 1000""" fg="white", ) ) - retrieval_model["reranking_model"]["reranking_provider_name"] = ( - f"{DEFAULT_PLUGIN_ID}/{retrieval_model['reranking_model']['reranking_provider_name']}/{retrieval_model['reranking_model']['reranking_provider_name']}" - ) + # update google to langgenius/gemini/google etc. + retrieval_model["reranking_model"]["reranking_provider_name"] = ModelProviderID( + retrieval_model["reranking_model"]["reranking_provider_name"] + ).to_string() retrieval_model_changed = True click.echo( @@ -86,9 +87,11 @@ limit 1000""" update_retrieval_model_sql = ", retrieval_model = :retrieval_model" params["retrieval_model"] = json.dumps(retrieval_model) + params["provider_name"] = ModelProviderID(provider_name).to_string() + sql = f"""update {table_name} set {provider_column_name} = - concat('{DEFAULT_PLUGIN_ID}/', {provider_column_name}, '/', {provider_column_name}) + :provider_name {update_retrieval_model_sql} where id = :record_id""" conn.execute(db.text(sql), params) @@ -122,7 +125,9 @@ limit 1000""" ) @classmethod - def migrate_db_records(cls, table_name: str, provider_column_name: str) -> None: + def migrate_db_records( + cls, table_name: str, provider_column_name: str, provider_cls: type[GenericProviderID] + ) -> None: click.echo(click.style(f"Migrating [{table_name}] data for plugin", fg="white")) processed_count = 0 @@ -166,7 +171,8 @@ limit 1000""" ) try: - updated_value = f"{DEFAULT_PLUGIN_ID}/{provider_name}/{provider_name}" + # update jina to langgenius/jina_tool/jina etc. + updated_value = provider_cls(provider_name).to_string() batch_updates.append((updated_value, record_id)) except Exception as e: failed_ids.append(record_id) diff --git a/api/services/plugin/oauth_service.py b/api/services/plugin/oauth_service.py index 461247419b..b84dd0afc5 100644 --- a/api/services/plugin/oauth_service.py +++ b/api/services/plugin/oauth_service.py @@ -1,7 +1,53 @@ +import json +import uuid + from core.plugin.impl.base import BasePluginClient +from extensions.ext_redis import redis_client + + +class OAuthProxyService(BasePluginClient): + # Default max age for proxy context parameter in seconds + __MAX_AGE__ = 5 * 60 # 5 minutes + __KEY_PREFIX__ = "oauth_proxy_context:" + + @staticmethod + def create_proxy_context(user_id: str, tenant_id: str, plugin_id: str, provider: str): + """ + Create a proxy context for an OAuth 2.0 authorization request. + + This parameter is a crucial security measure to prevent Cross-Site Request + Forgery (CSRF) attacks. It works by generating a unique nonce and storing it + in a distributed cache (Redis) along with the user's session context. + The returned nonce should be included as the 'proxy_context' parameter in the + authorization URL. Upon callback, the `use_proxy_context` method + is used to verify the state, ensuring the request's integrity and authenticity, + and mitigating replay attacks. + """ + context_id = str(uuid.uuid4()) + data = { + "user_id": user_id, + "plugin_id": plugin_id, + "tenant_id": tenant_id, + "provider": provider, + } + redis_client.setex( + f"{OAuthProxyService.__KEY_PREFIX__}{context_id}", + OAuthProxyService.__MAX_AGE__, + json.dumps(data), + ) + return context_id -class OAuthService(BasePluginClient): - @classmethod - def get_authorization_url(cls, tenant_id: str, user_id: str, provider_name: str) -> str: - return "1234567890" + @staticmethod + def use_proxy_context(context_id: str): + """ + Validate the proxy context parameter. + This checks if the context_id is valid and not expired. + """ + if not context_id: + raise ValueError("context_id is required") + # get data from redis + data = redis_client.getdel(f"{OAuthProxyService.__KEY_PREFIX__}{context_id}") + if not data: + raise ValueError("context_id is invalid") + return json.loads(data) diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py new file mode 100644 index 0000000000..393213c0e2 --- /dev/null +++ b/api/services/plugin/plugin_parameter_service.py @@ -0,0 +1,74 @@ +from collections.abc import Mapping, Sequence +from typing import Any, Literal + +from sqlalchemy.orm import Session + +from core.plugin.entities.parameters import PluginParameterOption +from core.plugin.impl.dynamic_select import DynamicSelectClient +from core.tools.tool_manager import ToolManager +from core.tools.utils.configuration import ProviderConfigEncrypter +from extensions.ext_database import db +from models.tools import BuiltinToolProvider + + +class PluginParameterService: + @staticmethod + def get_dynamic_select_options( + tenant_id: str, + user_id: str, + plugin_id: str, + provider: str, + action: str, + parameter: str, + provider_type: Literal["tool"], + ) -> Sequence[PluginParameterOption]: + """ + Get dynamic select options for a plugin parameter. + + Args: + tenant_id: The tenant ID. + plugin_id: The plugin ID. + provider: The provider name. + action: The action name. + parameter: The parameter name. + """ + credentials: Mapping[str, Any] = {} + + match provider_type: + case "tool": + provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) + # init tool configuration + tool_configuration = ProviderConfigEncrypter( + tenant_id=tenant_id, + config=[x.to_basic_provider_config() for x in provider_controller.get_credentials_schema()], + provider_type=provider_controller.provider_type.value, + provider_identity=provider_controller.entity.identity.name, + ) + + # check if credentials are required + if not provider_controller.need_credentials: + credentials = {} + else: + # fetch credentials from db + with Session(db.engine) as session: + db_record = ( + session.query(BuiltinToolProvider) + .filter( + BuiltinToolProvider.tenant_id == tenant_id, + BuiltinToolProvider.provider == provider, + ) + .first() + ) + + if db_record is None: + raise ValueError(f"Builtin provider {provider} not found when fetching credentials") + + credentials = tool_configuration.decrypt(db_record.credentials) + case _: + raise ValueError(f"Invalid provider type: {provider_type}") + + return ( + DynamicSelectClient() + .fetch_dynamic_select_options(tenant_id, user_id, plugin_id, provider, action, credentials, parameter) + .options + ) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index a8b64f27db..0f22afd8dd 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -17,11 +17,18 @@ from core.plugin.entities.plugin import ( PluginInstallation, PluginInstallationSource, ) -from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginListResponse, PluginUploadResponse +from core.plugin.entities.plugin_daemon import ( + PluginDecodeResponse, + PluginInstallTask, + PluginListResponse, + PluginVerification, +) from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.plugin import PluginInstaller from extensions.ext_redis import redis_client +from services.errors.plugin import PluginInstallationForbiddenError +from services.feature_service import FeatureService, PluginInstallationScope logger = logging.getLogger(__name__) @@ -86,6 +93,42 @@ class PluginService: logger.exception("failed to fetch latest plugin version") return result + @staticmethod + def _check_marketplace_only_permission(): + """ + Check if the marketplace only permission is enabled + """ + features = FeatureService.get_system_features() + if features.plugin_installation_permission.restrict_to_marketplace_only: + raise PluginInstallationForbiddenError("Plugin installation is restricted to marketplace only") + + @staticmethod + def _check_plugin_installation_scope(plugin_verification: Optional[PluginVerification]): + """ + Check the plugin installation scope + """ + features = FeatureService.get_system_features() + + match features.plugin_installation_permission.plugin_installation_scope: + case PluginInstallationScope.OFFICIAL_ONLY: + if ( + plugin_verification is None + or plugin_verification.authorized_category != PluginVerification.AuthorizedCategory.Langgenius + ): + raise PluginInstallationForbiddenError("Plugin installation is restricted to official only") + case PluginInstallationScope.OFFICIAL_AND_SPECIFIC_PARTNERS: + if plugin_verification is None or plugin_verification.authorized_category not in [ + PluginVerification.AuthorizedCategory.Langgenius, + PluginVerification.AuthorizedCategory.Partner, + ]: + raise PluginInstallationForbiddenError( + "Plugin installation is restricted to official and specific partners" + ) + case PluginInstallationScope.NONE: + raise PluginInstallationForbiddenError("Installing plugins is not allowed") + case PluginInstallationScope.ALL: + pass + @staticmethod def get_debugging_key(tenant_id: str) -> str: """ @@ -208,6 +251,8 @@ class PluginService: # check if plugin pkg is already downloaded manager = PluginInstaller() + features = FeatureService.get_system_features() + try: manager.fetch_plugin_manifest(tenant_id, new_plugin_unique_identifier) # already downloaded, skip, and record install event @@ -215,7 +260,14 @@ class PluginService: except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(new_plugin_unique_identifier) - manager.upload_pkg(tenant_id, pkg, verify_signature=False) + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + + # check if the plugin is available to install + PluginService._check_plugin_installation_scope(response.verification) return manager.upgrade_plugin( tenant_id, @@ -239,6 +291,7 @@ class PluginService: """ Upgrade plugin with github """ + PluginService._check_marketplace_only_permission() manager = PluginInstaller() return manager.upgrade_plugin( tenant_id, @@ -253,33 +306,43 @@ class PluginService: ) @staticmethod - def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginUploadResponse: + def upload_pkg(tenant_id: str, pkg: bytes, verify_signature: bool = False) -> PluginDecodeResponse: """ Upload plugin package files returns: plugin_unique_identifier """ + PluginService._check_marketplace_only_permission() manager = PluginInstaller() - return manager.upload_pkg(tenant_id, pkg, verify_signature) + features = FeatureService.get_system_features() + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + return response @staticmethod def upload_pkg_from_github( tenant_id: str, repo: str, version: str, package: str, verify_signature: bool = False - ) -> PluginUploadResponse: + ) -> PluginDecodeResponse: """ Install plugin from github release package files, returns plugin_unique_identifier """ + PluginService._check_marketplace_only_permission() pkg = download_with_size_limit( f"https://github.com/{repo}/releases/download/{version}/{package}", dify_config.PLUGIN_MAX_PACKAGE_SIZE ) + features = FeatureService.get_system_features() manager = PluginInstaller() - return manager.upload_pkg( + response = manager.upload_pkg( tenant_id, pkg, - verify_signature, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, ) + return response @staticmethod def upload_bundle( @@ -289,11 +352,15 @@ class PluginService: Upload a plugin bundle and return the dependencies. """ manager = PluginInstaller() + PluginService._check_marketplace_only_permission() return manager.upload_bundle(tenant_id, bundle, verify_signature) @staticmethod def install_from_local_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]): + PluginService._check_marketplace_only_permission() + manager = PluginInstaller() + return manager.install_from_identifiers( tenant_id, plugin_unique_identifiers, @@ -307,6 +374,8 @@ class PluginService: Install plugin from github release package files, returns plugin_unique_identifier """ + PluginService._check_marketplace_only_permission() + manager = PluginInstaller() return manager.install_from_identifiers( tenant_id, @@ -322,28 +391,33 @@ class PluginService: ) @staticmethod - def fetch_marketplace_pkg( - tenant_id: str, plugin_unique_identifier: str, verify_signature: bool = False - ) -> PluginDeclaration: + def fetch_marketplace_pkg(tenant_id: str, plugin_unique_identifier: str) -> PluginDeclaration: """ Fetch marketplace package """ if not dify_config.MARKETPLACE_ENABLED: raise ValueError("marketplace is not enabled") + features = FeatureService.get_system_features() + manager = PluginInstaller() try: declaration = manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) except Exception: pkg = download_plugin_pkg(plugin_unique_identifier) - declaration = manager.upload_pkg(tenant_id, pkg, verify_signature).manifest + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + # check if the plugin is available to install + PluginService._check_plugin_installation_scope(response.verification) + declaration = response.manifest return declaration @staticmethod - def install_from_marketplace_pkg( - tenant_id: str, plugin_unique_identifiers: Sequence[str], verify_signature: bool = False - ): + def install_from_marketplace_pkg(tenant_id: str, plugin_unique_identifiers: Sequence[str]): """ Install plugin from marketplace package files, returns installation task id @@ -353,26 +427,40 @@ class PluginService: manager = PluginInstaller() + # collect actual plugin_unique_identifiers + actual_plugin_unique_identifiers = [] + metas = [] + features = FeatureService.get_system_features() + # check if already downloaded for plugin_unique_identifier in plugin_unique_identifiers: try: manager.fetch_plugin_manifest(tenant_id, plugin_unique_identifier) + plugin_decode_response = manager.decode_plugin_from_identifier(tenant_id, plugin_unique_identifier) + # check if the plugin is available to install + PluginService._check_plugin_installation_scope(plugin_decode_response.verification) # already downloaded, skip + actual_plugin_unique_identifiers.append(plugin_unique_identifier) + metas.append({"plugin_unique_identifier": plugin_unique_identifier}) except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(plugin_unique_identifier) - manager.upload_pkg(tenant_id, pkg, verify_signature) + response = manager.upload_pkg( + tenant_id, + pkg, + verify_signature=features.plugin_installation_permission.restrict_to_marketplace_only, + ) + # check if the plugin is available to install + PluginService._check_plugin_installation_scope(response.verification) + # use response plugin_unique_identifier + actual_plugin_unique_identifiers.append(response.unique_identifier) + metas.append({"plugin_unique_identifier": response.unique_identifier}) return manager.install_from_identifiers( tenant_id, - plugin_unique_identifiers, + actual_plugin_unique_identifiers, PluginInstallationSource.Marketplace, - [ - { - "plugin_unique_identifier": plugin_unique_identifier, - } - for plugin_unique_identifier in plugin_unique_identifiers - ], + metas, ) @staticmethod diff --git a/api/services/tools/builtin_tools_manage_service.py b/api/services/tools/builtin_tools_manage_service.py index 3ccd14415d..58a4b2f179 100644 --- a/api/services/tools/builtin_tools_manage_service.py +++ b/api/services/tools/builtin_tools_manage_service.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from configs import dify_config from core.helper.position_helper import is_filtered from core.model_runtime.utils.encoders import jsonable_encoder -from core.plugin.entities.plugin import GenericProviderID, ToolProviderID +from core.plugin.entities.plugin import ToolProviderID from core.plugin.impl.exc import PluginDaemonClientSideError from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity @@ -290,7 +290,7 @@ class BuiltinToolManageService: def _fetch_builtin_provider(provider_name: str, tenant_id: str) -> BuiltinToolProvider | None: try: full_provider_name = provider_name - provider_id_entity = GenericProviderID(provider_name) + provider_id_entity = ToolProviderID(provider_name) provider_name = provider_id_entity.provider_name if provider_id_entity.organization != "langgenius": provider_obj = ( @@ -315,7 +315,7 @@ class BuiltinToolManageService: if provider_obj is None: return None - provider_obj.provider = GenericProviderID(provider_obj.provider).to_string() + provider_obj.provider = ToolProviderID(provider_obj.provider).to_string() return provider_obj except Exception: # it's an old provider without organization diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py new file mode 100644 index 0000000000..7c23abda4b --- /dev/null +++ b/api/services/tools/mcp_tools_mange_service.py @@ -0,0 +1,231 @@ +import hashlib +import json +from datetime import datetime +from typing import Any + +from sqlalchemy import or_ +from sqlalchemy.exc import IntegrityError + +from core.helper import encrypter +from core.mcp.error import MCPAuthError, MCPError +from core.mcp.mcp_client import MCPClient +from core.tools.entities.api_entities import ToolProviderApiEntity +from core.tools.entities.common_entities import I18nObject +from core.tools.entities.tool_entities import ToolProviderType +from core.tools.mcp_tool.provider import MCPToolProviderController +from core.tools.utils.configuration import ProviderConfigEncrypter +from extensions.ext_database import db +from models.tools import MCPToolProvider +from services.tools.tools_transform_service import ToolTransformService + +UNCHANGED_SERVER_URL_PLACEHOLDER = "[__HIDDEN__]" + + +class MCPToolManageService: + """ + Service class for managing mcp tools. + """ + + @staticmethod + def get_mcp_provider_by_provider_id(provider_id: str, tenant_id: str) -> MCPToolProvider: + res = ( + db.session.query(MCPToolProvider) + .filter(MCPToolProvider.tenant_id == tenant_id, MCPToolProvider.id == provider_id) + .first() + ) + if not res: + raise ValueError("MCP tool not found") + return res + + @staticmethod + def get_mcp_provider_by_server_identifier(server_identifier: str, tenant_id: str) -> MCPToolProvider: + res = ( + db.session.query(MCPToolProvider) + .filter(MCPToolProvider.tenant_id == tenant_id, MCPToolProvider.server_identifier == server_identifier) + .first() + ) + if not res: + raise ValueError("MCP tool not found") + return res + + @staticmethod + def create_mcp_provider( + tenant_id: str, + name: str, + server_url: str, + user_id: str, + icon: str, + icon_type: str, + icon_background: str, + server_identifier: str, + ) -> ToolProviderApiEntity: + server_url_hash = hashlib.sha256(server_url.encode()).hexdigest() + existing_provider = ( + db.session.query(MCPToolProvider) + .filter( + MCPToolProvider.tenant_id == tenant_id, + or_( + MCPToolProvider.name == name, + MCPToolProvider.server_url_hash == server_url_hash, + MCPToolProvider.server_identifier == server_identifier, + ), + ) + .first() + ) + if existing_provider: + if existing_provider.name == name: + raise ValueError(f"MCP tool {name} already exists") + elif existing_provider.server_url_hash == server_url_hash: + raise ValueError(f"MCP tool {server_url} already exists") + elif existing_provider.server_identifier == server_identifier: + raise ValueError(f"MCP tool {server_identifier} already exists") + encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) + mcp_tool = MCPToolProvider( + tenant_id=tenant_id, + name=name, + server_url=encrypted_server_url, + server_url_hash=server_url_hash, + user_id=user_id, + authed=False, + tools="[]", + icon=json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon, + server_identifier=server_identifier, + ) + db.session.add(mcp_tool) + db.session.commit() + return ToolTransformService.mcp_provider_to_user_provider(mcp_tool, for_list=True) + + @staticmethod + def retrieve_mcp_tools(tenant_id: str, for_list: bool = False) -> list[ToolProviderApiEntity]: + mcp_providers = ( + db.session.query(MCPToolProvider) + .filter(MCPToolProvider.tenant_id == tenant_id) + .order_by(MCPToolProvider.name) + .all() + ) + return [ + ToolTransformService.mcp_provider_to_user_provider(mcp_provider, for_list=for_list) + for mcp_provider in mcp_providers + ] + + @classmethod + def list_mcp_tool_from_remote_server(cls, tenant_id: str, provider_id: str): + mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + + try: + with MCPClient( + mcp_provider.decrypted_server_url, provider_id, tenant_id, authed=mcp_provider.authed, for_list=True + ) as mcp_client: + tools = mcp_client.list_tools() + except MCPAuthError as e: + raise ValueError("Please auth the tool first") + except MCPError as e: + raise ValueError(f"Failed to connect to MCP server: {e}") + mcp_provider.tools = json.dumps([tool.model_dump() for tool in tools]) + mcp_provider.authed = True + mcp_provider.updated_at = datetime.now() + db.session.commit() + user = mcp_provider.load_user() + return ToolProviderApiEntity( + id=mcp_provider.id, + name=mcp_provider.name, + tools=ToolTransformService.mcp_tool_to_user_tool(mcp_provider, tools), + type=ToolProviderType.MCP, + icon=mcp_provider.icon, + author=user.name if user else "Anonymous", + server_url=mcp_provider.masked_server_url, + updated_at=int(mcp_provider.updated_at.timestamp()), + description=I18nObject(en_US="", zh_Hans=""), + label=I18nObject(en_US=mcp_provider.name, zh_Hans=mcp_provider.name), + plugin_unique_identifier=mcp_provider.server_identifier, + ) + + @classmethod + def delete_mcp_tool(cls, tenant_id: str, provider_id: str): + mcp_tool = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + + db.session.delete(mcp_tool) + db.session.commit() + + @classmethod + def update_mcp_provider( + cls, + tenant_id: str, + provider_id: str, + name: str, + server_url: str, + icon: str, + icon_type: str, + icon_background: str, + server_identifier: str, + ): + mcp_provider = cls.get_mcp_provider_by_provider_id(provider_id, tenant_id) + mcp_provider.updated_at = datetime.now() + mcp_provider.name = name + mcp_provider.icon = ( + json.dumps({"content": icon, "background": icon_background}) if icon_type == "emoji" else icon + ) + mcp_provider.server_identifier = server_identifier + + if UNCHANGED_SERVER_URL_PLACEHOLDER not in server_url: + encrypted_server_url = encrypter.encrypt_token(tenant_id, server_url) + mcp_provider.server_url = encrypted_server_url + server_url_hash = hashlib.sha256(server_url.encode()).hexdigest() + + if server_url_hash != mcp_provider.server_url_hash: + cls._re_connect_mcp_provider(mcp_provider, provider_id, tenant_id) + mcp_provider.server_url_hash = server_url_hash + try: + db.session.commit() + except IntegrityError as e: + db.session.rollback() + error_msg = str(e.orig) + if "unique_mcp_provider_name" in error_msg: + raise ValueError(f"MCP tool {name} already exists") + elif "unique_mcp_provider_server_url" in error_msg: + raise ValueError(f"MCP tool {server_url} already exists") + elif "unique_mcp_provider_server_identifier" in error_msg: + raise ValueError(f"MCP tool {server_identifier} already exists") + else: + raise + + @classmethod + def update_mcp_provider_credentials( + cls, mcp_provider: MCPToolProvider, credentials: dict[str, Any], authed: bool = False + ): + provider_controller = MCPToolProviderController._from_db(mcp_provider) + tool_configuration = ProviderConfigEncrypter( + tenant_id=mcp_provider.tenant_id, + config=list(provider_controller.get_credentials_schema()), + provider_type=provider_controller.provider_type.value, + provider_identity=provider_controller.provider_id, + ) + credentials = tool_configuration.encrypt(credentials) + mcp_provider.updated_at = datetime.now() + mcp_provider.encrypted_credentials = json.dumps({**mcp_provider.credentials, **credentials}) + mcp_provider.authed = authed + if not authed: + mcp_provider.tools = "[]" + db.session.commit() + + @classmethod + def _re_connect_mcp_provider(cls, mcp_provider: MCPToolProvider, provider_id: str, tenant_id: str): + """re-connect mcp provider""" + try: + with MCPClient( + mcp_provider.decrypted_server_url, + provider_id, + tenant_id, + authed=False, + for_list=True, + ) as mcp_client: + tools = mcp_client.list_tools() + mcp_provider.authed = True + mcp_provider.tools = json.dumps([tool.model_dump() for tool in tools]) + except MCPAuthError: + mcp_provider.authed = False + mcp_provider.tools = "[]" + except MCPError as e: + raise ValueError(f"Failed to re-connect MCP server: {e}") from e + # reset credentials + mcp_provider.encrypted_credentials = "{}" diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 367121125b..3d0c35cd9b 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -1,10 +1,11 @@ import json import logging -from typing import Optional, Union, cast +from typing import Any, Optional, Union, cast from yarl import URL from configs import dify_config +from core.mcp.types import Tool as MCPTool from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.builtin_tool.provider import BuiltinToolProviderController @@ -21,7 +22,7 @@ from core.tools.plugin_tool.provider import PluginToolProviderController from core.tools.utils.configuration import ProviderConfigEncrypter from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.tools.workflow_as_tool.tool import WorkflowTool -from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider +from models.tools import ApiToolProvider, BuiltinToolProvider, MCPToolProvider, WorkflowToolProvider logger = logging.getLogger(__name__) @@ -52,7 +53,8 @@ class ToolTransformService: return icon except Exception: return {"background": "#252525", "content": "\ud83d\ude01"} - + elif provider_type == ToolProviderType.MCP.value: + return icon return "" @staticmethod @@ -73,10 +75,18 @@ class ToolTransformService: provider.icon = ToolTransformService.get_plugin_icon_url( tenant_id=tenant_id, filename=provider.icon ) + if isinstance(provider.icon_dark, str) and provider.icon_dark: + provider.icon_dark = ToolTransformService.get_plugin_icon_url( + tenant_id=tenant_id, filename=provider.icon_dark + ) else: provider.icon = ToolTransformService.get_tool_provider_icon_url( provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon ) + if provider.icon_dark: + provider.icon_dark = ToolTransformService.get_tool_provider_icon_url( + provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon_dark + ) @classmethod def builtin_provider_to_user_provider( @@ -94,6 +104,7 @@ class ToolTransformService: name=provider_controller.entity.identity.name, description=provider_controller.entity.identity.description, icon=provider_controller.entity.identity.icon, + icon_dark=provider_controller.entity.identity.icon_dark, label=provider_controller.entity.identity.label, type=ToolProviderType.BUILT_IN, masked_credentials={}, @@ -148,11 +159,16 @@ class ToolTransformService: convert provider controller to user provider """ # package tool provider controller + auth_type = ApiProviderAuthType.NONE + credentials_auth_type = db_provider.credentials.get("auth_type") + if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif credentials_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( db_provider=db_provider, - auth_type=ApiProviderAuthType.API_KEY - if db_provider.credentials["auth_type"] == "api_key" - else ApiProviderAuthType.NONE, + auth_type=auth_type, ) return controller @@ -177,6 +193,7 @@ class ToolTransformService: name=provider_controller.entity.identity.name, description=provider_controller.entity.identity.description, icon=provider_controller.entity.identity.icon, + icon_dark=provider_controller.entity.identity.icon_dark, label=provider_controller.entity.identity.label, type=ToolProviderType.WORKFLOW, masked_credentials={}, @@ -187,6 +204,41 @@ class ToolTransformService: labels=labels or [], ) + @staticmethod + def mcp_provider_to_user_provider(db_provider: MCPToolProvider, for_list: bool = False) -> ToolProviderApiEntity: + user = db_provider.load_user() + return ToolProviderApiEntity( + id=db_provider.server_identifier if not for_list else db_provider.id, + author=user.name if user else "Anonymous", + name=db_provider.name, + icon=db_provider.provider_icon, + type=ToolProviderType.MCP, + is_team_authorization=db_provider.authed, + server_url=db_provider.masked_server_url, + tools=ToolTransformService.mcp_tool_to_user_tool( + db_provider, [MCPTool(**tool) for tool in json.loads(db_provider.tools)] + ), + updated_at=int(db_provider.updated_at.timestamp()), + label=I18nObject(en_US=db_provider.name, zh_Hans=db_provider.name), + description=I18nObject(en_US="", zh_Hans=""), + server_identifier=db_provider.server_identifier, + ) + + @staticmethod + def mcp_tool_to_user_tool(mcp_provider: MCPToolProvider, tools: list[MCPTool]) -> list[ToolApiEntity]: + user = mcp_provider.load_user() + return [ + ToolApiEntity( + author=user.name if user else "Anonymous", + name=tool.name, + label=I18nObject(en_US=tool.name, zh_Hans=tool.name), + description=I18nObject(en_US=tool.description, zh_Hans=tool.description), + parameters=ToolTransformService.convert_mcp_schema_to_parameter(tool.inputSchema), + labels=[], + ) + for tool in tools + ] + @classmethod def api_provider_to_user_provider( cls, @@ -304,3 +356,53 @@ class ToolTransformService: parameters=tool.parameters, labels=labels or [], ) + + @staticmethod + def convert_mcp_schema_to_parameter(schema: dict) -> list["ToolParameter"]: + """ + Convert MCP JSON schema to tool parameters + + :param schema: JSON schema dictionary + :return: list of ToolParameter instances + """ + + def create_parameter( + name: str, description: str, param_type: str, required: bool, input_schema: dict | None = None + ) -> ToolParameter: + """Create a ToolParameter instance with given attributes""" + input_schema_dict: dict[str, Any] = {"input_schema": input_schema} if input_schema else {} + return ToolParameter( + name=name, + llm_description=description, + label=I18nObject(en_US=name), + form=ToolParameter.ToolParameterForm.LLM, + required=required, + type=ToolParameter.ToolParameterType(param_type), + human_description=I18nObject(en_US=description), + **input_schema_dict, + ) + + def process_properties(props: dict, required: list, prefix: str = "") -> list[ToolParameter]: + """Process properties recursively""" + TYPE_MAPPING = {"integer": "number", "float": "number"} + COMPLEX_TYPES = ["array", "object"] + + parameters = [] + for name, prop in props.items(): + current_description = prop.get("description", "") + prop_type = prop.get("type", "string") + + if isinstance(prop_type, list): + prop_type = prop_type[0] + if prop_type in TYPE_MAPPING: + prop_type = TYPE_MAPPING[prop_type] + input_schema = prop if prop_type in COMPLEX_TYPES else None + parameters.append( + create_parameter(name, current_description, prop_type, name in required, input_schema) + ) + + return parameters + + if schema.get("type") == "object" and "properties" in schema: + return process_properties(schema["properties"], schema.get("required", [])) + return [] diff --git a/api/services/vector_service.py b/api/services/vector_service.py index 19e37f4ee3..9165139193 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -97,16 +97,16 @@ class VectorService: vector = Vector(dataset=dataset) vector.delete_by_ids([segment.index_node_id]) vector.add_texts([document], duplicate_check=True) - - # update keyword index - keyword = Keyword(dataset) - keyword.delete_by_ids([segment.index_node_id]) - - # save keyword index - if keywords and len(keywords) > 0: - keyword.add_texts([document], keywords_list=[keywords]) else: - keyword.add_texts([document]) + # update keyword index + keyword = Keyword(dataset) + keyword.delete_by_ids([segment.index_node_id]) + + # save keyword index + if keywords and len(keywords) > 0: + keyword.add_texts([document], keywords_list=[keywords]) + else: + keyword.add_texts([document]) @classmethod def generate_child_chunks( diff --git a/api/services/webapp_auth_service.py b/api/services/webapp_auth_service.py index 79d5217de7..8f92b3f070 100644 --- a/api/services/webapp_auth_service.py +++ b/api/services/webapp_auth_service.py @@ -1,31 +1,38 @@ -import random +import enum +import secrets from datetime import UTC, datetime, timedelta from typing import Any, Optional, cast from werkzeug.exceptions import NotFound, Unauthorized from configs import dify_config -from controllers.web.error import WebAppAuthAccessDeniedError from extensions.ext_database import db from libs.helper import TokenManager from libs.passport import PassportService from libs.password import compare_password from models.account import Account, AccountStatus from models.model import App, EndUser, Site +from services.app_service import AppService from services.enterprise.enterprise_service import EnterpriseService from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError -from services.feature_service import FeatureService from tasks.mail_email_code_login import send_email_code_login_mail_task +class WebAppAuthType(enum.StrEnum): + """Enum for web app authentication types.""" + + PUBLIC = "public" + INTERNAL = "internal" + EXTERNAL = "external" + + class WebAppAuthService: """Service for web app authentication.""" @staticmethod def authenticate(email: str, password: str) -> Account: """authenticate account with email and password""" - - account = Account.query.filter_by(email=email).first() + account = db.session.query(Account).filter_by(email=email).first() if not account: raise AccountNotFoundError() @@ -38,12 +45,8 @@ class WebAppAuthService: return cast(Account, account) @classmethod - def login(cls, account: Account, app_code: str, end_user_id: str) -> str: - site = db.session.query(Site).filter(Site.code == app_code).first() - if not site: - raise NotFound("Site not found.") - - access_token = cls._get_account_jwt_token(account=account, site=site, end_user_id=end_user_id) + def login(cls, account: Account) -> str: + access_token = cls._get_account_jwt_token(account=account) return access_token @@ -66,9 +69,9 @@ class WebAppAuthService: if email is None: raise ValueError("Email must be provided.") - code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + code = "".join([str(secrets.randbelow(exclusive_upper_bound=10)) for _ in range(6)]) token = TokenManager.generate_token( - account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code} + account=account, email=email, token_type="email_code_login", additional_data={"code": code} ) send_email_code_login_mail_task.delay( language=language, @@ -80,11 +83,11 @@ class WebAppAuthService: @classmethod def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]: - return TokenManager.get_token_data(token, "webapp_email_code_login") + return TokenManager.get_token_data(token, "email_code_login") @classmethod def revoke_email_code_login_token(cls, token: str): - TokenManager.revoke_token(token, "webapp_email_code_login") + TokenManager.revoke_token(token, "email_code_login") @classmethod def create_end_user(cls, app_code, email) -> EndUser: @@ -109,33 +112,67 @@ class WebAppAuthService: return end_user @classmethod - def _validate_user_accessibility(cls, account: Account, app_code: str): - """Check if the user is allowed to access the app.""" - system_features = FeatureService.get_system_features() - if system_features.webapp_auth.enabled: - app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code) - - if ( - app_settings.access_mode != "public" - and not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(account.id, app_code=app_code) - ): - raise WebAppAuthAccessDeniedError() - - @classmethod - def _get_account_jwt_token(cls, account: Account, site: Site, end_user_id: str) -> str: + def _get_account_jwt_token(cls, account: Account) -> str: exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES * 24) exp = int(exp_dt.timestamp()) payload = { - "iss": site.id, "sub": "Web API Passport", - "app_id": site.app_id, - "app_code": site.code, "user_id": account.id, - "end_user_id": end_user_id, - "token_source": "webapp", + "session_id": account.email, + "token_source": "webapp_login_token", + "auth_type": "internal", "exp": exp, } token: str = PassportService().issue(payload) return token + + @classmethod + def is_app_require_permission_check( + cls, app_code: Optional[str] = None, app_id: Optional[str] = None, access_mode: Optional[str] = None + ) -> bool: + """ + Check if the app requires permission check based on its access mode. + """ + modes_requiring_permission_check = [ + "private", + "private_all", + ] + if access_mode: + return access_mode in modes_requiring_permission_check + + if not app_code and not app_id: + raise ValueError("Either app_code or app_id must be provided.") + + if app_code: + app_id = AppService.get_app_id_by_code(app_code) + if not app_id: + raise ValueError("App ID could not be determined from the provided app_code.") + + webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id) + if webapp_settings and webapp_settings.access_mode in modes_requiring_permission_check: + return True + return False + + @classmethod + def get_app_auth_type(cls, app_code: str | None = None, access_mode: str | None = None) -> WebAppAuthType: + """ + Get the authentication type for the app based on its access mode. + """ + if not app_code and not access_mode: + raise ValueError("Either app_code or access_mode must be provided.") + + if access_mode: + if access_mode == "public": + return WebAppAuthType.PUBLIC + elif access_mode in ["private", "private_all"]: + return WebAppAuthType.INTERNAL + elif access_mode == "sso_verified": + return WebAppAuthType.EXTERNAL + + if app_code: + webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code) + return cls.get_app_auth_type(access_mode=webapp_settings.access_mode) + + raise ValueError("Could not determine app authentication type.") diff --git a/api/services/workflow_app_service.py b/api/services/workflow_app_service.py index 6b30a70372..6eabf03018 100644 --- a/api/services/workflow_app_service.py +++ b/api/services/workflow_app_service.py @@ -5,7 +5,7 @@ from sqlalchemy import and_, func, or_, select from sqlalchemy.orm import Session from core.workflow.entities.workflow_execution import WorkflowExecutionStatus -from models import App, EndUser, WorkflowAppLog, WorkflowRun +from models import Account, App, EndUser, WorkflowAppLog, WorkflowRun from models.enums import CreatorUserRole @@ -21,6 +21,8 @@ class WorkflowAppService: created_at_after: datetime | None = None, page: int = 1, limit: int = 20, + created_by_end_user_session_id: str | None = None, + created_by_account: str | None = None, ) -> dict: """ Get paginate workflow app logs using SQLAlchemy 2.0 style @@ -32,6 +34,8 @@ class WorkflowAppService: :param created_at_after: filter logs created after this timestamp :param page: page number :param limit: items per page + :param created_by_end_user_session_id: filter by end user session id + :param created_by_account: filter by account email :return: Pagination object """ # Build base statement using SQLAlchemy 2.0 style @@ -71,6 +75,26 @@ class WorkflowAppService: if created_at_after: stmt = stmt.where(WorkflowAppLog.created_at >= created_at_after) + # Filter by end user session id or account email + if created_by_end_user_session_id: + stmt = stmt.join( + EndUser, + and_( + WorkflowAppLog.created_by == EndUser.id, + WorkflowAppLog.created_by_role == CreatorUserRole.END_USER, + EndUser.session_id == created_by_end_user_session_id, + ), + ) + if created_by_account: + stmt = stmt.join( + Account, + and_( + WorkflowAppLog.created_by == Account.id, + WorkflowAppLog.created_by_role == CreatorUserRole.ACCOUNT, + Account.email == created_by_account, + ), + ) + stmt = stmt.order_by(WorkflowAppLog.created_at.desc()) # Get total count using the same filters diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py new file mode 100644 index 0000000000..f306e1f062 --- /dev/null +++ b/api/services/workflow_draft_variable_service.py @@ -0,0 +1,746 @@ +import dataclasses +import datetime +import logging +from collections.abc import Mapping, Sequence +from enum import StrEnum +from typing import Any, ClassVar + +from sqlalchemy import Engine, orm +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.sql.expression import and_, or_ + +from core.app.entities.app_invoke_entities import InvokeFrom +from core.file.models import File +from core.variables import Segment, StringSegment, Variable +from core.variables.consts import MIN_SELECTORS_LENGTH +from core.variables.segments import ArrayFileSegment, FileSegment +from core.variables.types import SegmentType +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from core.workflow.enums import SystemVariableKey +from core.workflow.nodes import NodeType +from core.workflow.nodes.variable_assigner.common.helpers import get_updated_variables +from core.workflow.variable_loader import VariableLoader +from factories.file_factory import StorageKeyLoader +from factories.variable_factory import build_segment, segment_to_variable +from models import App, Conversation +from models.enums import DraftVariableType +from models.workflow import Workflow, WorkflowDraftVariable, is_system_variable_editable +from repositories.factory import DifyAPIRepositoryFactory + +_logger = logging.getLogger(__name__) + + +@dataclasses.dataclass(frozen=True) +class WorkflowDraftVariableList: + variables: list[WorkflowDraftVariable] + total: int | None = None + + +class WorkflowDraftVariableError(Exception): + pass + + +class VariableResetError(WorkflowDraftVariableError): + pass + + +class UpdateNotSupportedError(WorkflowDraftVariableError): + pass + + +class DraftVarLoader(VariableLoader): + # This implements the VariableLoader interface for loading draft variables. + # + # ref: core.workflow.variable_loader.VariableLoader + + # Database engine used for loading variables. + _engine: Engine + # Application ID for which variables are being loaded. + _app_id: str + _tenant_id: str + _fallback_variables: Sequence[Variable] + + def __init__( + self, + engine: Engine, + app_id: str, + tenant_id: str, + fallback_variables: Sequence[Variable] | None = None, + ) -> None: + self._engine = engine + self._app_id = app_id + self._tenant_id = tenant_id + self._fallback_variables = fallback_variables or [] + + def _selector_to_tuple(self, selector: Sequence[str]) -> tuple[str, str]: + return (selector[0], selector[1]) + + def load_variables(self, selectors: list[list[str]]) -> list[Variable]: + if not selectors: + return [] + + # Map each selector (as a tuple via `_selector_to_tuple`) to its corresponding Variable instance. + variable_by_selector: dict[tuple[str, str], Variable] = {} + + with Session(bind=self._engine, expire_on_commit=False) as session: + srv = WorkflowDraftVariableService(session) + draft_vars = srv.get_draft_variables_by_selectors(self._app_id, selectors) + + for draft_var in draft_vars: + segment = draft_var.get_value() + variable = segment_to_variable( + segment=segment, + selector=draft_var.get_selector(), + id=draft_var.id, + name=draft_var.name, + description=draft_var.description, + ) + selector_tuple = self._selector_to_tuple(variable.selector) + variable_by_selector[selector_tuple] = variable + + # Important: + files: list[File] = [] + for draft_var in draft_vars: + value = draft_var.get_value() + if isinstance(value, FileSegment): + files.append(value.value) + elif isinstance(value, ArrayFileSegment): + files.extend(value.value) + with Session(bind=self._engine) as session: + storage_key_loader = StorageKeyLoader(session, tenant_id=self._tenant_id) + storage_key_loader.load_storage_keys(files) + + return list(variable_by_selector.values()) + + +class WorkflowDraftVariableService: + _session: Session + + def __init__(self, session: Session) -> None: + """ + Initialize the WorkflowDraftVariableService with a SQLAlchemy session. + + Args: + session (Session): The SQLAlchemy session used to execute database queries. + The provided session must be bound to an `Engine` object, not a specific `Connection`. + + Raises: + AssertionError: If the provided session is not bound to an `Engine` object. + """ + self._session = session + engine = session.get_bind() + # Ensure the session is bound to a engine. + assert isinstance(engine, Engine) + session_maker = sessionmaker(bind=engine, expire_on_commit=False) + self._api_node_execution_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository( + session_maker + ) + + def get_variable(self, variable_id: str) -> WorkflowDraftVariable | None: + return self._session.query(WorkflowDraftVariable).filter(WorkflowDraftVariable.id == variable_id).first() + + def get_draft_variables_by_selectors( + self, + app_id: str, + selectors: Sequence[list[str]], + ) -> list[WorkflowDraftVariable]: + ors = [] + for selector in selectors: + assert len(selector) >= MIN_SELECTORS_LENGTH, f"Invalid selector to get: {selector}" + node_id, name = selector[:2] + ors.append(and_(WorkflowDraftVariable.node_id == node_id, WorkflowDraftVariable.name == name)) + + # NOTE(QuantumGhost): Although the number of `or` expressions may be large, as long as + # each expression includes conditions on both `node_id` and `name` (which are covered by the unique index), + # PostgreSQL can efficiently retrieve the results using a bitmap index scan. + # + # Alternatively, a `SELECT` statement could be constructed for each selector and + # combined using `UNION` to fetch all rows. + # Benchmarking indicates that both approaches yield comparable performance. + variables = ( + self._session.query(WorkflowDraftVariable).where(WorkflowDraftVariable.app_id == app_id, or_(*ors)).all() + ) + return variables + + def list_variables_without_values(self, app_id: str, page: int, limit: int) -> WorkflowDraftVariableList: + criteria = WorkflowDraftVariable.app_id == app_id + total = None + query = self._session.query(WorkflowDraftVariable).filter(criteria) + if page == 1: + total = query.count() + variables = ( + # Do not load the `value` field. + query.options(orm.defer(WorkflowDraftVariable.value)) + .order_by(WorkflowDraftVariable.created_at.desc()) + .limit(limit) + .offset((page - 1) * limit) + .all() + ) + + return WorkflowDraftVariableList(variables=variables, total=total) + + def _list_node_variables(self, app_id: str, node_id: str) -> WorkflowDraftVariableList: + criteria = ( + WorkflowDraftVariable.app_id == app_id, + WorkflowDraftVariable.node_id == node_id, + ) + query = self._session.query(WorkflowDraftVariable).filter(*criteria) + variables = query.order_by(WorkflowDraftVariable.created_at.desc()).all() + return WorkflowDraftVariableList(variables=variables) + + def list_node_variables(self, app_id: str, node_id: str) -> WorkflowDraftVariableList: + return self._list_node_variables(app_id, node_id) + + def list_conversation_variables(self, app_id: str) -> WorkflowDraftVariableList: + return self._list_node_variables(app_id, CONVERSATION_VARIABLE_NODE_ID) + + def list_system_variables(self, app_id: str) -> WorkflowDraftVariableList: + return self._list_node_variables(app_id, SYSTEM_VARIABLE_NODE_ID) + + def get_conversation_variable(self, app_id: str, name: str) -> WorkflowDraftVariable | None: + return self._get_variable(app_id=app_id, node_id=CONVERSATION_VARIABLE_NODE_ID, name=name) + + def get_system_variable(self, app_id: str, name: str) -> WorkflowDraftVariable | None: + return self._get_variable(app_id=app_id, node_id=SYSTEM_VARIABLE_NODE_ID, name=name) + + def get_node_variable(self, app_id: str, node_id: str, name: str) -> WorkflowDraftVariable | None: + return self._get_variable(app_id, node_id, name) + + def _get_variable(self, app_id: str, node_id: str, name: str) -> WorkflowDraftVariable | None: + variable = ( + self._session.query(WorkflowDraftVariable) + .where( + WorkflowDraftVariable.app_id == app_id, + WorkflowDraftVariable.node_id == node_id, + WorkflowDraftVariable.name == name, + ) + .first() + ) + return variable + + def update_variable( + self, + variable: WorkflowDraftVariable, + name: str | None = None, + value: Segment | None = None, + ) -> WorkflowDraftVariable: + if not variable.editable: + raise UpdateNotSupportedError(f"variable not support updating, id={variable.id}") + if name is not None: + variable.set_name(name) + if value is not None: + variable.set_value(value) + variable.last_edited_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + self._session.flush() + return variable + + def _reset_conv_var(self, workflow: Workflow, variable: WorkflowDraftVariable) -> WorkflowDraftVariable | None: + conv_var_by_name = {i.name: i for i in workflow.conversation_variables} + conv_var = conv_var_by_name.get(variable.name) + + if conv_var is None: + self._session.delete(instance=variable) + self._session.flush() + _logger.warning( + "Conversation variable not found for draft variable, id=%s, name=%s", variable.id, variable.name + ) + return None + + variable.set_value(conv_var) + variable.last_edited_at = None + self._session.add(variable) + self._session.flush() + return variable + + def _reset_node_var_or_sys_var( + self, workflow: Workflow, variable: WorkflowDraftVariable + ) -> WorkflowDraftVariable | None: + # If a variable does not allow updating, it makes no sence to resetting it. + if not variable.editable: + return variable + # No execution record for this variable, delete the variable instead. + if variable.node_execution_id is None: + self._session.delete(instance=variable) + self._session.flush() + _logger.warning("draft variable has no node_execution_id, id=%s, name=%s", variable.id, variable.name) + return None + + node_exec = self._api_node_execution_repo.get_execution_by_id(variable.node_execution_id) + if node_exec is None: + _logger.warning( + "Node exectution not found for draft variable, id=%s, name=%s, node_execution_id=%s", + variable.id, + variable.name, + variable.node_execution_id, + ) + self._session.delete(instance=variable) + self._session.flush() + return None + + outputs_dict = node_exec.outputs_dict or {} + # a sentinel value used to check the absent of the output variable key. + absent = object() + + if variable.get_variable_type() == DraftVariableType.NODE: + # Get node type for proper value extraction + node_config = workflow.get_node_config_by_id(variable.node_id) + node_type = workflow.get_node_type_from_node_config(node_config) + + # Note: Based on the implementation in `_build_from_variable_assigner_mapping`, + # VariableAssignerNode (both v1 and v2) can only create conversation draft variables. + # For consistency, we should simply return when processing VARIABLE_ASSIGNER nodes. + # + # This implementation must remain synchronized with the `_build_from_variable_assigner_mapping` + # and `save` methods. + if node_type == NodeType.VARIABLE_ASSIGNER: + return variable + output_value = outputs_dict.get(variable.name, absent) + else: + output_value = outputs_dict.get(f"sys.{variable.name}", absent) + + # We cannot use `is None` to check the existence of an output variable here as + # the value of the output may be `None`. + if output_value is absent: + # If variable not found in execution data, delete the variable + self._session.delete(instance=variable) + self._session.flush() + return None + value_seg = WorkflowDraftVariable.build_segment_with_type(variable.value_type, output_value) + # Extract variable value using unified logic + variable.set_value(value_seg) + variable.last_edited_at = None # Reset to indicate this is a reset operation + self._session.flush() + return variable + + def reset_variable(self, workflow: Workflow, variable: WorkflowDraftVariable) -> WorkflowDraftVariable | None: + variable_type = variable.get_variable_type() + if variable_type == DraftVariableType.SYS and not is_system_variable_editable(variable.name): + raise VariableResetError(f"cannot reset system variable, variable_id={variable.id}") + if variable_type == DraftVariableType.CONVERSATION: + return self._reset_conv_var(workflow, variable) + else: + return self._reset_node_var_or_sys_var(workflow, variable) + + def delete_variable(self, variable: WorkflowDraftVariable): + self._session.delete(variable) + + def delete_workflow_variables(self, app_id: str): + ( + self._session.query(WorkflowDraftVariable) + .filter(WorkflowDraftVariable.app_id == app_id) + .delete(synchronize_session=False) + ) + + def delete_node_variables(self, app_id: str, node_id: str): + return self._delete_node_variables(app_id, node_id) + + def _delete_node_variables(self, app_id: str, node_id: str): + self._session.query(WorkflowDraftVariable).where( + WorkflowDraftVariable.app_id == app_id, + WorkflowDraftVariable.node_id == node_id, + ).delete() + + def _get_conversation_id_from_draft_variable(self, app_id: str) -> str | None: + draft_var = self._get_variable( + app_id=app_id, + node_id=SYSTEM_VARIABLE_NODE_ID, + name=str(SystemVariableKey.CONVERSATION_ID), + ) + if draft_var is None: + return None + segment = draft_var.get_value() + if not isinstance(segment, StringSegment): + _logger.warning( + "sys.conversation_id variable is not a string: app_id=%s, id=%s", + app_id, + draft_var.id, + ) + return None + return segment.value + + def get_or_create_conversation( + self, + account_id: str, + app: App, + workflow: Workflow, + ) -> str: + """ + get_or_create_conversation creates and returns the ID of a conversation for debugging. + + If a conversation already exists, as determined by the following criteria, its ID is returned: + - The system variable `sys.conversation_id` exists in the draft variable table, and + - A corresponding conversation record is found in the database. + + If no such conversation exists, a new conversation is created and its ID is returned. + """ + conv_id = self._get_conversation_id_from_draft_variable(workflow.app_id) + + if conv_id is not None: + conversation = ( + self._session.query(Conversation) + .filter( + Conversation.id == conv_id, + Conversation.app_id == workflow.app_id, + ) + .first() + ) + # Only return the conversation ID if it exists and is valid (has a correspond conversation record in DB). + if conversation is not None: + return conv_id + conversation = Conversation( + app_id=workflow.app_id, + app_model_config_id=app.app_model_config_id, + model_provider=None, + model_id="", + override_model_configs=None, + mode=app.mode, + name="Draft Debugging Conversation", + inputs={}, + introduction="", + system_instruction="", + system_instruction_tokens=0, + status="normal", + invoke_from=InvokeFrom.DEBUGGER.value, + from_source="console", + from_end_user_id=None, + from_account_id=account_id, + ) + + self._session.add(conversation) + self._session.flush() + return conversation.id + + def prefill_conversation_variable_default_values(self, workflow: Workflow): + """""" + draft_conv_vars: list[WorkflowDraftVariable] = [] + for conv_var in workflow.conversation_variables: + draft_var = WorkflowDraftVariable.new_conversation_variable( + app_id=workflow.app_id, + name=conv_var.name, + value=conv_var, + description=conv_var.description, + ) + draft_conv_vars.append(draft_var) + _batch_upsert_draft_varaible( + self._session, + draft_conv_vars, + policy=_UpsertPolicy.IGNORE, + ) + + +class _UpsertPolicy(StrEnum): + IGNORE = "ignore" + OVERWRITE = "overwrite" + + +def _batch_upsert_draft_varaible( + session: Session, + draft_vars: Sequence[WorkflowDraftVariable], + policy: _UpsertPolicy = _UpsertPolicy.OVERWRITE, +) -> None: + if not draft_vars: + return None + # Although we could use SQLAlchemy ORM operations here, we choose not to for several reasons: + # + # 1. The variable saving process involves writing multiple rows to the + # `workflow_draft_variables` table. Batch insertion significantly improves performance. + # 2. Using the ORM would require either: + # + # a. Checking for the existence of each variable before insertion, + # resulting in 2n SQL statements for n variables and potential concurrency issues. + # b. Attempting insertion first, then updating if a unique index violation occurs, + # which still results in n to 2n SQL statements. + # + # Both approaches are inefficient and suboptimal. + # 3. We do not need to retrieve the results of the SQL execution or populate ORM + # model instances with the returned values. + # 4. Batch insertion with `ON CONFLICT DO UPDATE` allows us to insert or update all + # variables in a single SQL statement, avoiding the issues above. + # + # For these reasons, we use the SQLAlchemy query builder and rely on dialect-specific + # insert operations instead of the ORM layer. + stmt = insert(WorkflowDraftVariable).values([_model_to_insertion_dict(v) for v in draft_vars]) + if policy == _UpsertPolicy.OVERWRITE: + stmt = stmt.on_conflict_do_update( + index_elements=WorkflowDraftVariable.unique_app_id_node_id_name(), + set_={ + # Refresh creation timestamp to ensure updated variables + # appear first in chronologically sorted result sets. + "created_at": stmt.excluded.created_at, + "updated_at": stmt.excluded.updated_at, + "last_edited_at": stmt.excluded.last_edited_at, + "description": stmt.excluded.description, + "value_type": stmt.excluded.value_type, + "value": stmt.excluded.value, + "visible": stmt.excluded.visible, + "editable": stmt.excluded.editable, + "node_execution_id": stmt.excluded.node_execution_id, + }, + ) + elif _UpsertPolicy.IGNORE: + stmt = stmt.on_conflict_do_nothing(index_elements=WorkflowDraftVariable.unique_app_id_node_id_name()) + else: + raise Exception("Invalid value for update policy.") + session.execute(stmt) + + +def _model_to_insertion_dict(model: WorkflowDraftVariable) -> dict[str, Any]: + d: dict[str, Any] = { + "app_id": model.app_id, + "last_edited_at": None, + "node_id": model.node_id, + "name": model.name, + "selector": model.selector, + "value_type": model.value_type, + "value": model.value, + "node_execution_id": model.node_execution_id, + } + if model.visible is not None: + d["visible"] = model.visible + if model.editable is not None: + d["editable"] = model.editable + if model.created_at is not None: + d["created_at"] = model.created_at + if model.updated_at is not None: + d["updated_at"] = model.updated_at + if model.description is not None: + d["description"] = model.description + return d + + +def _build_segment_for_serialized_values(v: Any) -> Segment: + """ + Reconstructs Segment objects from serialized values, with special handling + for FileSegment and ArrayFileSegment types. + + This function should only be used when: + 1. No explicit type information is available + 2. The input value is in serialized form (dict or list) + + It detects potential file objects in the serialized data and properly rebuilds the + appropriate segment type. + """ + return build_segment(WorkflowDraftVariable.rebuild_file_types(v)) + + +class DraftVariableSaver: + # _DUMMY_OUTPUT_IDENTITY is a placeholder output for workflow nodes. + # Its sole possible value is `None`. + # + # This is used to signal the execution of a workflow node when it has no other outputs. + _DUMMY_OUTPUT_IDENTITY: ClassVar[str] = "__dummy__" + _DUMMY_OUTPUT_VALUE: ClassVar[None] = None + + # _EXCLUDE_VARIABLE_NAMES_MAPPING maps node types and versions to variable names that + # should be excluded when saving draft variables. This prevents certain internal or + # technical variables from being exposed in the draft environment, particularly those + # that aren't meant to be directly edited or viewed by users. + _EXCLUDE_VARIABLE_NAMES_MAPPING: dict[NodeType, frozenset[str]] = { + NodeType.LLM: frozenset(["finish_reason"]), + NodeType.LOOP: frozenset(["loop_round"]), + } + + # Database session used for persisting draft variables. + _session: Session + + # The application ID associated with the draft variables. + # This should match the `Workflow.app_id` of the workflow to which the current node belongs. + _app_id: str + + # The ID of the node for which DraftVariableSaver is saving output variables. + _node_id: str + + # The type of the current node (see NodeType). + _node_type: NodeType + + # + _node_execution_id: str + + # _enclosing_node_id identifies the container node that the current node belongs to. + # For example, if the current node is an LLM node inside an Iteration node + # or Loop node, then `_enclosing_node_id` refers to the ID of + # the containing Iteration or Loop node. + # + # If the current node is not nested within another node, `_enclosing_node_id` is + # `None`. + _enclosing_node_id: str | None + + def __init__( + self, + session: Session, + app_id: str, + node_id: str, + node_type: NodeType, + node_execution_id: str, + enclosing_node_id: str | None = None, + ): + # Important: `node_execution_id` parameter refers to the primary key (`id`) of the + # WorkflowNodeExecutionModel/WorkflowNodeExecution, not their `node_execution_id` + # field. These are distinct database fields with different purposes. + self._session = session + self._app_id = app_id + self._node_id = node_id + self._node_type = node_type + self._node_execution_id = node_execution_id + self._enclosing_node_id = enclosing_node_id + + def _create_dummy_output_variable(self): + return WorkflowDraftVariable.new_node_variable( + app_id=self._app_id, + node_id=self._node_id, + name=self._DUMMY_OUTPUT_IDENTITY, + node_execution_id=self._node_execution_id, + value=build_segment(self._DUMMY_OUTPUT_VALUE), + visible=False, + editable=False, + ) + + def _should_save_output_variables_for_draft(self) -> bool: + if self._enclosing_node_id is not None and self._node_type != NodeType.VARIABLE_ASSIGNER: + # Currently we do not save output variables for nodes inside loop or iteration. + return False + return True + + def _build_from_variable_assigner_mapping(self, process_data: Mapping[str, Any]) -> list[WorkflowDraftVariable]: + draft_vars: list[WorkflowDraftVariable] = [] + updated_variables = get_updated_variables(process_data) or [] + + for item in updated_variables: + selector = item.selector + if len(selector) < MIN_SELECTORS_LENGTH: + raise Exception("selector too short") + # NOTE(QuantumGhost): only the following two kinds of variable could be updated by + # VariableAssigner: ConversationVariable and iteration variable. + # We only save conversation variable here. + if selector[0] != CONVERSATION_VARIABLE_NODE_ID: + continue + segment = WorkflowDraftVariable.build_segment_with_type(segment_type=item.value_type, value=item.new_value) + draft_vars.append( + WorkflowDraftVariable.new_conversation_variable( + app_id=self._app_id, + name=item.name, + value=segment, + ) + ) + # Add a dummy output variable to indicate that this node is executed. + draft_vars.append(self._create_dummy_output_variable()) + return draft_vars + + def _build_variables_from_start_mapping(self, output: Mapping[str, Any]) -> list[WorkflowDraftVariable]: + draft_vars = [] + has_non_sys_variables = False + for name, value in output.items(): + value_seg = _build_segment_for_serialized_values(value) + node_id, name = self._normalize_variable_for_start_node(name) + # If node_id is not `sys`, it means that the variable is a user-defined input field + # in `Start` node. + if node_id != SYSTEM_VARIABLE_NODE_ID: + draft_vars.append( + WorkflowDraftVariable.new_node_variable( + app_id=self._app_id, + node_id=self._node_id, + name=name, + node_execution_id=self._node_execution_id, + value=value_seg, + visible=True, + editable=True, + ) + ) + has_non_sys_variables = True + else: + if name == SystemVariableKey.FILES: + # Here we know the type of variable must be `array[file]`, we + # just build files from the value. + files = [File.model_validate(v) for v in value] + if files: + value_seg = WorkflowDraftVariable.build_segment_with_type(SegmentType.ARRAY_FILE, files) + else: + value_seg = ArrayFileSegment(value=[]) + + draft_vars.append( + WorkflowDraftVariable.new_sys_variable( + app_id=self._app_id, + name=name, + node_execution_id=self._node_execution_id, + value=value_seg, + editable=self._should_variable_be_editable(node_id, name), + ) + ) + if not has_non_sys_variables: + draft_vars.append(self._create_dummy_output_variable()) + return draft_vars + + def _normalize_variable_for_start_node(self, name: str) -> tuple[str, str]: + if not name.startswith(f"{SYSTEM_VARIABLE_NODE_ID}."): + return self._node_id, name + _, name_ = name.split(".", maxsplit=1) + return SYSTEM_VARIABLE_NODE_ID, name_ + + def _build_variables_from_mapping(self, output: Mapping[str, Any]) -> list[WorkflowDraftVariable]: + draft_vars = [] + for name, value in output.items(): + if not self._should_variable_be_saved(name): + _logger.debug( + "Skip saving variable as it has been excluded by its node_type, name=%s, node_type=%s", + name, + self._node_type, + ) + continue + if isinstance(value, Segment): + value_seg = value + else: + value_seg = _build_segment_for_serialized_values(value) + draft_vars.append( + WorkflowDraftVariable.new_node_variable( + app_id=self._app_id, + node_id=self._node_id, + name=name, + node_execution_id=self._node_execution_id, + value=value_seg, + visible=self._should_variable_be_visible(self._node_id, self._node_type, name), + ) + ) + return draft_vars + + def save( + self, + process_data: Mapping[str, Any] | None = None, + outputs: Mapping[str, Any] | None = None, + ): + draft_vars: list[WorkflowDraftVariable] = [] + if outputs is None: + outputs = {} + if process_data is None: + process_data = {} + if not self._should_save_output_variables_for_draft(): + return + if self._node_type == NodeType.VARIABLE_ASSIGNER: + draft_vars = self._build_from_variable_assigner_mapping(process_data=process_data) + elif self._node_type == NodeType.START: + draft_vars = self._build_variables_from_start_mapping(outputs) + else: + draft_vars = self._build_variables_from_mapping(outputs) + _batch_upsert_draft_varaible(self._session, draft_vars) + + @staticmethod + def _should_variable_be_editable(node_id: str, name: str) -> bool: + if node_id in (CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID): + return False + if node_id == SYSTEM_VARIABLE_NODE_ID and not is_system_variable_editable(name): + return False + return True + + @staticmethod + def _should_variable_be_visible(node_id: str, node_type: NodeType, name: str) -> bool: + if node_type in NodeType.IF_ELSE: + return False + if node_id == SYSTEM_VARIABLE_NODE_ID and not is_system_variable_editable(name): + return False + return True + + def _should_variable_be_saved(self, name: str) -> bool: + exclude_var_names = self._EXCLUDE_VARIABLE_NAMES_MAPPING.get(self._node_type) + if exclude_var_names is None: + return True + return name not in exclude_var_names diff --git a/api/services/workflow_run_service.py b/api/services/workflow_run_service.py index 483c0d3086..e43999a8c9 100644 --- a/api/services/workflow_run_service.py +++ b/api/services/workflow_run_service.py @@ -2,9 +2,9 @@ import threading from collections.abc import Sequence from typing import Optional +from sqlalchemy.orm import sessionmaker + import contexts -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository -from core.workflow.repositories.workflow_node_execution_repository import OrderConfig from extensions.ext_database import db from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import ( @@ -15,10 +15,18 @@ from models import ( WorkflowRun, WorkflowRunTriggeredFrom, ) -from models.workflow import WorkflowNodeExecutionTriggeredFrom +from repositories.factory import DifyAPIRepositoryFactory class WorkflowRunService: + def __init__(self): + """Initialize WorkflowRunService with repository dependencies.""" + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + self._node_execution_service_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository( + session_maker + ) + self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + def get_paginate_advanced_chat_workflow_runs(self, app_model: App, args: dict) -> InfiniteScrollPagination: """ Get advanced chat app workflow run list @@ -62,45 +70,16 @@ class WorkflowRunService: :param args: request args """ limit = int(args.get("limit", 20)) + last_id = args.get("last_id") - base_query = db.session.query(WorkflowRun).filter( - WorkflowRun.tenant_id == app_model.tenant_id, - WorkflowRun.app_id == app_model.id, - WorkflowRun.triggered_from == WorkflowRunTriggeredFrom.DEBUGGING.value, + return self._workflow_run_repo.get_paginated_workflow_runs( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + triggered_from=WorkflowRunTriggeredFrom.DEBUGGING.value, + limit=limit, + last_id=last_id, ) - if args.get("last_id"): - last_workflow_run = base_query.filter( - WorkflowRun.id == args.get("last_id"), - ).first() - - if not last_workflow_run: - raise ValueError("Last workflow run not exists") - - workflow_runs = ( - base_query.filter( - WorkflowRun.created_at < last_workflow_run.created_at, WorkflowRun.id != last_workflow_run.id - ) - .order_by(WorkflowRun.created_at.desc()) - .limit(limit) - .all() - ) - else: - workflow_runs = base_query.order_by(WorkflowRun.created_at.desc()).limit(limit).all() - - has_more = False - if len(workflow_runs) == limit: - current_page_first_workflow_run = workflow_runs[-1] - rest_count = base_query.filter( - WorkflowRun.created_at < current_page_first_workflow_run.created_at, - WorkflowRun.id != current_page_first_workflow_run.id, - ).count() - - if rest_count > 0: - has_more = True - - return InfiniteScrollPagination(data=workflow_runs, limit=limit, has_more=has_more) - def get_workflow_run(self, app_model: App, run_id: str) -> Optional[WorkflowRun]: """ Get workflow run detail @@ -108,18 +87,12 @@ class WorkflowRunService: :param app_model: app model :param run_id: workflow run id """ - workflow_run = ( - db.session.query(WorkflowRun) - .filter( - WorkflowRun.tenant_id == app_model.tenant_id, - WorkflowRun.app_id == app_model.id, - WorkflowRun.id == run_id, - ) - .first() + return self._workflow_run_repo.get_workflow_run_by_id( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + run_id=run_id, ) - return workflow_run - def get_workflow_run_node_executions( self, app_model: App, @@ -137,17 +110,13 @@ class WorkflowRunService: if not workflow_run: return [] - repository = SQLAlchemyWorkflowNodeExecutionRepository( - session_factory=db.engine, - user=user, - app_id=app_model.id, - triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, - ) + # Get tenant_id from user + tenant_id = user.tenant_id if isinstance(user, EndUser) else user.current_tenant_id + if tenant_id is None: + raise ValueError("User tenant_id cannot be None") - # Use the repository to get the database models directly - order_config = OrderConfig(order_by=["index"], order_direction="desc") - workflow_node_executions = repository.get_db_models_by_workflow_run( - workflow_run_id=run_id, order_config=order_config + return self._node_execution_service_repo.get_executions_by_workflow_run( + tenant_id=tenant_id, + app_id=app_model.id, + workflow_run_id=run_id, ) - - return workflow_node_executions diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index bc213ccce6..677bc74237 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -1,18 +1,23 @@ import json import time -from collections.abc import Callable, Generator, Sequence +import uuid +from collections.abc import Callable, Generator, Mapping, Sequence from datetime import UTC, datetime -from typing import Any, Optional +from typing import Any, Optional, cast from uuid import uuid4 from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker +from core.app.app_config.entities import VariableEntityType from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.file import File +from core.repositories import DifyCoreRepositoryFactory from core.variables import Variable +from core.variables.variables import VariableUnion from core.workflow.entities.node_entities import NodeRunResult +from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecution, WorkflowNodeExecutionStatus from core.workflow.errors import WorkflowNodeRunFailedError from core.workflow.graph_engine.entities.event import InNodeEvent @@ -22,9 +27,12 @@ from core.workflow.nodes.enums import ErrorStrategy from core.workflow.nodes.event import RunCompletedEvent from core.workflow.nodes.event.types import NodeEvent from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING +from core.workflow.nodes.start.entities import StartNodeData +from core.workflow.system_variable import SystemVariable from core.workflow.workflow_entry import WorkflowEntry from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated from extensions.ext_database import db +from factories.file_factory import build_from_mapping, build_from_mappings from models.account import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider @@ -34,10 +42,16 @@ from models.workflow import ( WorkflowNodeExecutionTriggeredFrom, WorkflowType, ) -from services.errors.app import WorkflowHashNotEqualError +from repositories.factory import DifyAPIRepositoryFactory +from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError from services.workflow.workflow_converter import WorkflowConverter from .errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError +from .workflow_draft_variable_service import ( + DraftVariableSaver, + DraftVarLoader, + WorkflowDraftVariableService, +) class WorkflowService: @@ -45,6 +59,44 @@ class WorkflowService: Workflow Service """ + def __init__(self, session_maker: sessionmaker | None = None): + """Initialize WorkflowService with repository dependencies.""" + if session_maker is None: + session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + self._node_execution_service_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository( + session_maker + ) + + def get_node_last_run(self, app_model: App, workflow: Workflow, node_id: str) -> WorkflowNodeExecutionModel | None: + """ + Get the most recent execution for a specific node. + + Args: + app_model: The application model + workflow: The workflow model + node_id: The node identifier + + Returns: + The most recent WorkflowNodeExecutionModel for the node, or None if not found + """ + return self._node_execution_service_repo.get_node_last_execution( + tenant_id=app_model.tenant_id, + app_id=app_model.id, + workflow_id=workflow.id, + node_id=node_id, + ) + + def is_workflow_exist(self, app_model: App) -> bool: + return ( + db.session.query(Workflow) + .filter( + Workflow.tenant_id == app_model.tenant_id, + Workflow.app_id == app_model.id, + Workflow.version == Workflow.VERSION_DRAFT, + ) + .count() + ) > 0 + def get_draft_workflow(self, app_model: App) -> Optional[Workflow]: """ Get draft workflow @@ -61,6 +113,23 @@ class WorkflowService: # return draft workflow return workflow + def get_published_workflow_by_id(self, app_model: App, workflow_id: str) -> Optional[Workflow]: + # fetch published workflow by workflow_id + workflow = ( + db.session.query(Workflow) + .filter( + Workflow.tenant_id == app_model.tenant_id, + Workflow.app_id == app_model.id, + Workflow.id == workflow_id, + ) + .first() + ) + if not workflow: + return None + if workflow.version == Workflow.VERSION_DRAFT: + raise IsDraftWorkflowError(f"Workflow is draft version, id={workflow_id}") + return workflow + def get_published_workflow(self, app_model: App) -> Optional[Workflow]: """ Get published workflow @@ -199,7 +268,7 @@ class WorkflowService: tenant_id=app_model.tenant_id, app_id=app_model.id, type=draft_workflow.type, - version=str(datetime.now(UTC).replace(tzinfo=None)), + version=Workflow.version_from_datetime(datetime.now(UTC).replace(tzinfo=None)), graph=draft_workflow.graph, features=draft_workflow.features, created_by=account.id, @@ -253,26 +322,85 @@ class WorkflowService: return default_config def run_draft_workflow_node( - self, app_model: App, node_id: str, user_inputs: dict, account: Account + self, + app_model: App, + draft_workflow: Workflow, + node_id: str, + user_inputs: Mapping[str, Any], + account: Account, + query: str = "", + files: Sequence[File] | None = None, ) -> WorkflowNodeExecutionModel: """ Run draft workflow node """ - # fetch draft workflow by app_model - draft_workflow = self.get_draft_workflow(app_model=app_model) - if not draft_workflow: - raise ValueError("Workflow not initialized") + files = files or [] + + with Session(bind=db.engine, expire_on_commit=False) as session, session.begin(): + draft_var_srv = WorkflowDraftVariableService(session) + draft_var_srv.prefill_conversation_variable_default_values(draft_workflow) + + node_config = draft_workflow.get_node_config_by_id(node_id) + node_type = Workflow.get_node_type_from_node_config(node_config) + node_data = node_config.get("data", {}) + if node_type == NodeType.START: + with Session(bind=db.engine) as session, session.begin(): + draft_var_srv = WorkflowDraftVariableService(session) + conversation_id = draft_var_srv.get_or_create_conversation( + account_id=account.id, + app=app_model, + workflow=draft_workflow, + ) + start_data = StartNodeData.model_validate(node_data) + user_inputs = _rebuild_file_for_user_inputs_in_start_node( + tenant_id=draft_workflow.tenant_id, start_node_data=start_data, user_inputs=user_inputs + ) + # init variable pool + variable_pool = _setup_variable_pool( + query=query, + files=files or [], + user_id=account.id, + user_inputs=user_inputs, + workflow=draft_workflow, + # NOTE(QuantumGhost): We rely on `DraftVarLoader` to load conversation variables. + conversation_variables=[], + node_type=node_type, + conversation_id=conversation_id, + ) + + else: + variable_pool = VariablePool( + system_variables=SystemVariable.empty(), + user_inputs=user_inputs, + environment_variables=draft_workflow.environment_variables, + conversation_variables=[], + ) + + variable_loader = DraftVarLoader( + engine=db.engine, + app_id=app_model.id, + tenant_id=app_model.tenant_id, + ) + + eclosing_node_type_and_id = draft_workflow.get_enclosing_node_type_and_id(node_config) + if eclosing_node_type_and_id: + _, enclosing_node_id = eclosing_node_type_and_id + else: + enclosing_node_id = None + + run = WorkflowEntry.single_step_run( + workflow=draft_workflow, + node_id=node_id, + user_inputs=user_inputs, + user_id=account.id, + variable_pool=variable_pool, + variable_loader=variable_loader, + ) # run draft workflow node start_at = time.perf_counter() - node_execution = self._handle_node_run_result( - invoke_node_fn=lambda: WorkflowEntry.single_step_run( - workflow=draft_workflow, - node_id=node_id, - user_inputs=user_inputs, - user_id=account.id, - ), + invoke_node_fn=lambda: run, start_at=start_at, node_id=node_id, ) @@ -281,7 +409,7 @@ class WorkflowService: node_execution.workflow_id = draft_workflow.id # Create repository and save the node execution - repository = SQLAlchemyWorkflowNodeExecutionRepository( + repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( session_factory=db.engine, user=account, app_id=app_model.id, @@ -289,8 +417,21 @@ class WorkflowService: ) repository.save(node_execution) - # Convert node_execution to WorkflowNodeExecution after save - workflow_node_execution = repository.to_db_model(node_execution) + workflow_node_execution = self._node_execution_service_repo.get_execution_by_id(node_execution.id) + if workflow_node_execution is None: + raise ValueError(f"WorkflowNodeExecution with id {node_execution.id} not found after saving") + + with Session(bind=db.engine) as session, session.begin(): + draft_var_saver = DraftVariableSaver( + session=session, + app_id=app_model.id, + node_id=workflow_node_execution.node_id, + node_type=NodeType(workflow_node_execution.node_type), + enclosing_node_id=enclosing_node_id, + node_execution_id=node_execution.id, + ) + draft_var_saver.save(process_data=node_execution.process_data, outputs=node_execution.outputs) + session.commit() return workflow_node_execution @@ -303,7 +444,7 @@ class WorkflowService: # run draft workflow node start_at = time.perf_counter() - workflow_node_execution = self._handle_node_run_result( + node_execution = self._handle_node_run_result( invoke_node_fn=lambda: WorkflowEntry.run_free_node( node_id=node_id, node_data=node_data, @@ -315,7 +456,7 @@ class WorkflowService: node_id=node_id, ) - return workflow_node_execution + return node_execution def _handle_node_run_result( self, @@ -332,7 +473,7 @@ class WorkflowService: node_run_result = event.run_result # sign output files - node_run_result.outputs = WorkflowEntry.handle_special_values(node_run_result.outputs) + # node_run_result.outputs = WorkflowEntry.handle_special_values(node_run_result.outputs) break if not node_run_result: @@ -394,7 +535,7 @@ class WorkflowService: if node_run_result.process_data else None ) - outputs = WorkflowEntry.handle_special_values(node_run_result.outputs) if node_run_result.outputs else None + outputs = node_run_result.outputs node_execution.inputs = inputs node_execution.process_data = process_data @@ -531,3 +672,77 @@ class WorkflowService: session.delete(workflow) return True + + +def _setup_variable_pool( + query: str, + files: Sequence[File], + user_id: str, + user_inputs: Mapping[str, Any], + workflow: Workflow, + node_type: NodeType, + conversation_id: str, + conversation_variables: list[Variable], +): + # Only inject system variables for START node type. + if node_type == NodeType.START: + system_variable = SystemVariable( + user_id=user_id, + app_id=workflow.app_id, + workflow_id=workflow.id, + files=files or [], + workflow_execution_id=str(uuid.uuid4()), + ) + + # Only add chatflow-specific variables for non-workflow types + if workflow.type != WorkflowType.WORKFLOW.value: + system_variable.query = query + system_variable.conversation_id = conversation_id + system_variable.dialogue_count = 0 + else: + system_variable = SystemVariable.empty() + + # init variable pool + variable_pool = VariablePool( + system_variables=system_variable, + user_inputs=user_inputs, + environment_variables=workflow.environment_variables, + # Based on the definition of `VariableUnion`, + # `list[Variable]` can be safely used as `list[VariableUnion]` since they are compatible. + conversation_variables=cast(list[VariableUnion], conversation_variables), # + ) + + return variable_pool + + +def _rebuild_file_for_user_inputs_in_start_node( + tenant_id: str, start_node_data: StartNodeData, user_inputs: Mapping[str, Any] +) -> Mapping[str, Any]: + inputs_copy = dict(user_inputs) + + for variable in start_node_data.variables: + if variable.type not in (VariableEntityType.FILE, VariableEntityType.FILE_LIST): + continue + if variable.variable not in user_inputs: + continue + value = user_inputs[variable.variable] + file = _rebuild_single_file(tenant_id=tenant_id, value=value, variable_entity_type=variable.type) + inputs_copy[variable.variable] = file + return inputs_copy + + +def _rebuild_single_file(tenant_id: str, value: Any, variable_entity_type: VariableEntityType) -> File | Sequence[File]: + if variable_entity_type == VariableEntityType.FILE: + if not isinstance(value, dict): + raise ValueError(f"expected dict for file object, got {type(value)}") + return build_from_mapping(mapping=value, tenant_id=tenant_id) + elif variable_entity_type == VariableEntityType.FILE_LIST: + if not isinstance(value, list): + raise ValueError(f"expected list for file list object, got {type(value)}") + if len(value) == 0: + return [] + if not isinstance(value[0], dict): + raise ValueError(f"expected dict for first element in the file list, got {type(value)}") + return build_from_mappings(mappings=value, tenant_id=tenant_id) + else: + raise Exception("unreachable") diff --git a/api/tasks/clean_document_task.py b/api/tasks/clean_document_task.py index 5824121e8f..c72a3319c1 100644 --- a/api/tasks/clean_document_task.py +++ b/api/tasks/clean_document_task.py @@ -72,6 +72,7 @@ def clean_document_task(document_id: str, dataset_id: str, doc_form: str, file_i DatasetMetadataBinding.dataset_id == dataset_id, DatasetMetadataBinding.document_id == document_id, ).delete() + db.session.commit() end_at = time.perf_counter() logging.info( diff --git a/api/tasks/remove_app_and_related_data_task.py b/api/tasks/remove_app_and_related_data_task.py index d366efd6f2..179adcbd6e 100644 --- a/api/tasks/remove_app_and_related_data_task.py +++ b/api/tasks/remove_app_and_related_data_task.py @@ -6,6 +6,7 @@ import click from celery import shared_task # type: ignore from sqlalchemy import delete from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import sessionmaker from extensions.ext_database import db from models import ( @@ -13,6 +14,7 @@ from models import ( AppAnnotationHitHistory, AppAnnotationSetting, AppDatasetJoin, + AppMCPServer, AppModelConfig, Conversation, EndUser, @@ -30,7 +32,8 @@ from models import ( ) from models.tools import WorkflowToolProvider from models.web import PinnedConversation, SavedMessage -from models.workflow import ConversationVariable, Workflow, WorkflowAppLog, WorkflowNodeExecutionModel, WorkflowRun +from models.workflow import ConversationVariable, Workflow, WorkflowAppLog +from repositories.factory import DifyAPIRepositoryFactory @shared_task(queue="app_deletion", bind=True, max_retries=3) @@ -41,6 +44,7 @@ def remove_app_and_related_data_task(self, tenant_id: str, app_id: str): # Delete related data _delete_app_model_configs(tenant_id, app_id) _delete_app_site(tenant_id, app_id) + _delete_app_mcp_servers(tenant_id, app_id) _delete_app_api_tokens(tenant_id, app_id) _delete_installed_apps(tenant_id, app_id) _delete_recommended_apps(tenant_id, app_id) @@ -89,6 +93,18 @@ def _delete_app_site(tenant_id: str, app_id: str): _delete_records("""select id from sites where app_id=:app_id limit 1000""", {"app_id": app_id}, del_site, "site") +def _delete_app_mcp_servers(tenant_id: str, app_id: str): + def del_mcp_server(mcp_server_id: str): + db.session.query(AppMCPServer).filter(AppMCPServer.id == mcp_server_id).delete(synchronize_session=False) + + _delete_records( + """select id from app_mcp_servers where app_id=:app_id limit 1000""", + {"app_id": app_id}, + del_mcp_server, + "app mcp server", + ) + + def _delete_app_api_tokens(tenant_id: str, app_id: str): def del_api_token(api_token_id: str): db.session.query(ApiToken).filter(ApiToken.id == api_token_id).delete(synchronize_session=False) @@ -175,30 +191,32 @@ def _delete_app_workflows(tenant_id: str, app_id: str): def _delete_app_workflow_runs(tenant_id: str, app_id: str): - def del_workflow_run(workflow_run_id: str): - db.session.query(WorkflowRun).filter(WorkflowRun.id == workflow_run_id).delete(synchronize_session=False) - - _delete_records( - """select id from workflow_runs where tenant_id=:tenant_id and app_id=:app_id limit 1000""", - {"tenant_id": tenant_id, "app_id": app_id}, - del_workflow_run, - "workflow run", + """Delete all workflow runs for an app using the service repository.""" + session_maker = sessionmaker(bind=db.engine) + workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) + + deleted_count = workflow_run_repo.delete_runs_by_app( + tenant_id=tenant_id, + app_id=app_id, + batch_size=1000, ) + logging.info(f"Deleted {deleted_count} workflow runs for app {app_id}") -def _delete_app_workflow_node_executions(tenant_id: str, app_id: str): - def del_workflow_node_execution(workflow_node_execution_id: str): - db.session.query(WorkflowNodeExecutionModel).filter( - WorkflowNodeExecutionModel.id == workflow_node_execution_id - ).delete(synchronize_session=False) - _delete_records( - """select id from workflow_node_executions where tenant_id=:tenant_id and app_id=:app_id limit 1000""", - {"tenant_id": tenant_id, "app_id": app_id}, - del_workflow_node_execution, - "workflow node execution", +def _delete_app_workflow_node_executions(tenant_id: str, app_id: str): + """Delete all workflow node executions for an app using the service repository.""" + session_maker = sessionmaker(bind=db.engine) + node_execution_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository(session_maker) + + deleted_count = node_execution_repo.delete_executions_by_app( + tenant_id=tenant_id, + app_id=app_id, + batch_size=1000, ) + logging.info(f"Deleted {deleted_count} workflow node executions for app {app_id}") + def _delete_app_workflow_app_logs(tenant_id: str, app_id: str): def del_workflow_app_log(workflow_app_log_id: str): diff --git a/api/tasks/retry_document_indexing_task.py b/api/tasks/retry_document_indexing_task.py index a6e7092216..8f8c3f9d81 100644 --- a/api/tasks/retry_document_indexing_task.py +++ b/api/tasks/retry_document_indexing_task.py @@ -30,11 +30,11 @@ def retry_document_indexing_task(dataset_id: str, document_ids: list[str]): logging.info(click.style("Dataset not found: {}".format(dataset_id), fg="red")) db.session.close() return - + tenant_id = dataset.tenant_id for document_id in document_ids: retry_indexing_cache_key = "document_{}_is_retried".format(document_id) # check document limit - features = FeatureService.get_features(dataset.tenant_id) + features = FeatureService.get_features(tenant_id) try: if features.billing.enabled: vector_space = features.vector_space diff --git a/api/tests/integration_tests/.env.example b/api/tests/integration_tests/.env.example index 9e40a8494d..4046096c27 100644 --- a/api/tests/integration_tests/.env.example +++ b/api/tests/integration_tests/.env.example @@ -1,107 +1,217 @@ -# OpenAI API Key -OPENAI_API_KEY= +FLASK_APP=app.py +FLASK_DEBUG=0 +SECRET_KEY='uhySf6a3aZuvRNfAlcr47paOw9TRYBY6j8ZHXpVw1yx5RP27Yj3w2uvI' + +CONSOLE_API_URL=http://127.0.0.1:5001 +CONSOLE_WEB_URL=http://127.0.0.1:3000 + +# Service API base URL +SERVICE_API_URL=http://127.0.0.1:5001 + +# Web APP base URL +APP_WEB_URL=http://127.0.0.1:3000 + +# Files URL +FILES_URL=http://127.0.0.1:5001 + +# The time in seconds after the signature is rejected +FILES_ACCESS_TIMEOUT=300 + +# Access token expiration time in minutes +ACCESS_TOKEN_EXPIRE_MINUTES=60 + +# Refresh token expiration time in days +REFRESH_TOKEN_EXPIRE_DAYS=30 + +# celery configuration +CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1 + +# redis configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_USERNAME= +REDIS_PASSWORD=difyai123456 +REDIS_USE_SSL=false +REDIS_DB=0 + +# PostgreSQL database configuration +DB_USERNAME=postgres +DB_PASSWORD=difyai123456 +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE=dify + +# Storage configuration +# use for store upload files, private keys... +# storage type: opendal, s3, aliyun-oss, azure-blob, baidu-obs, google-storage, huawei-obs, oci-storage, tencent-cos, volcengine-tos, supabase +STORAGE_TYPE=opendal + +# Apache OpenDAL storage configuration, refer to https://github.com/apache/opendal +OPENDAL_SCHEME=fs +OPENDAL_FS_ROOT=storage + +# CORS configuration +WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* +CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* + +# Vector database configuration +# support: weaviate, qdrant, milvus, myscale, relyt, pgvecto_rs, pgvector, pgvector, chroma, opensearch, tidb_vector, couchbase, vikingdb, upstash, lindorm, oceanbase +VECTOR_STORE=weaviate +# Weaviate configuration +WEAVIATE_ENDPOINT=http://localhost:8080 +WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih +WEAVIATE_GRPC_ENABLED=false +WEAVIATE_BATCH_SIZE=100 + + +# Upload configuration +UPLOAD_FILE_SIZE_LIMIT=15 +UPLOAD_FILE_BATCH_LIMIT=5 +UPLOAD_IMAGE_FILE_SIZE_LIMIT=10 +UPLOAD_VIDEO_FILE_SIZE_LIMIT=100 +UPLOAD_AUDIO_FILE_SIZE_LIMIT=50 + +# Model configuration +MULTIMODAL_SEND_FORMAT=base64 +PROMPT_GENERATION_MAX_TOKENS=4096 +CODE_GENERATION_MAX_TOKENS=1024 + +# Mail configuration, support: resend, smtp +MAIL_TYPE= +MAIL_DEFAULT_SEND_FROM=no-reply +RESEND_API_KEY= +RESEND_API_URL=https://api.resend.com +# smtp configuration +SMTP_SERVER=smtp.example.com +SMTP_PORT=465 +SMTP_USERNAME=123 +SMTP_PASSWORD=abc +SMTP_USE_TLS=true +SMTP_OPPORTUNISTIC_TLS=false + +# Sentry configuration +SENTRY_DSN= + +# DEBUG +DEBUG=false +SQLALCHEMY_ECHO=false + +# Notion import configuration, support public and internal +NOTION_INTEGRATION_TYPE=public +NOTION_CLIENT_SECRET=you-client-secret +NOTION_CLIENT_ID=you-client-id +NOTION_INTERNAL_SECRET=you-internal-secret + +ETL_TYPE=dify +UNSTRUCTURED_API_URL= +UNSTRUCTURED_API_KEY= +SCARF_NO_ANALYTICS=false + +#ssrf +SSRF_PROXY_HTTP_URL= +SSRF_PROXY_HTTPS_URL= +SSRF_DEFAULT_MAX_RETRIES=3 +SSRF_DEFAULT_TIME_OUT=5 +SSRF_DEFAULT_CONNECT_TIME_OUT=5 +SSRF_DEFAULT_READ_TIME_OUT=5 +SSRF_DEFAULT_WRITE_TIME_OUT=5 + +BATCH_UPLOAD_LIMIT=10 +KEYWORD_DATA_SOURCE_TYPE=database + +# Workflow file upload limit +WORKFLOW_FILE_UPLOAD_LIMIT=10 -# Azure OpenAI API Base Endpoint & API Key -AZURE_OPENAI_API_BASE= -AZURE_OPENAI_API_KEY= - -# Anthropic API Key -ANTHROPIC_API_KEY= - -# Replicate API Key -REPLICATE_API_KEY= - -# Hugging Face API Key -HUGGINGFACE_API_KEY= -HUGGINGFACE_TEXT_GEN_ENDPOINT_URL= -HUGGINGFACE_TEXT2TEXT_GEN_ENDPOINT_URL= -HUGGINGFACE_EMBEDDINGS_ENDPOINT_URL= - -# Minimax Credentials -MINIMAX_API_KEY= -MINIMAX_GROUP_ID= - -# Spark Credentials -SPARK_APP_ID= -SPARK_API_KEY= -SPARK_API_SECRET= - -# Tongyi Credentials -TONGYI_DASHSCOPE_API_KEY= - -# Wenxin Credentials -WENXIN_API_KEY= -WENXIN_SECRET_KEY= - -# ZhipuAI Credentials -ZHIPUAI_API_KEY= - -# Baichuan Credentials -BAICHUAN_API_KEY= -BAICHUAN_SECRET_KEY= - -# ChatGLM Credentials -CHATGLM_API_BASE= - -# Xinference Credentials -XINFERENCE_SERVER_URL= -XINFERENCE_GENERATION_MODEL_UID= -XINFERENCE_CHAT_MODEL_UID= -XINFERENCE_EMBEDDINGS_MODEL_UID= -XINFERENCE_RERANK_MODEL_UID= - -# OpenLLM Credentials -OPENLLM_SERVER_URL= - -# LocalAI Credentials -LOCALAI_SERVER_URL= - -# Cohere Credentials -COHERE_API_KEY= - -# Jina Credentials -JINA_API_KEY= - -# Ollama Credentials -OLLAMA_BASE_URL= +# CODE EXECUTION CONFIGURATION +CODE_EXECUTION_ENDPOINT=http://127.0.0.1:8194 +CODE_EXECUTION_API_KEY=dify-sandbox +CODE_MAX_NUMBER=9223372036854775807 +CODE_MIN_NUMBER=-9223372036854775808 +CODE_MAX_STRING_LENGTH=80000 +TEMPLATE_TRANSFORM_MAX_LENGTH=80000 +CODE_MAX_STRING_ARRAY_LENGTH=30 +CODE_MAX_OBJECT_ARRAY_LENGTH=30 +CODE_MAX_NUMBER_ARRAY_LENGTH=1000 + +# API Tool configuration +API_TOOL_DEFAULT_CONNECT_TIMEOUT=10 +API_TOOL_DEFAULT_READ_TIMEOUT=60 + +# HTTP Node configuration +HTTP_REQUEST_MAX_CONNECT_TIMEOUT=300 +HTTP_REQUEST_MAX_READ_TIMEOUT=600 +HTTP_REQUEST_MAX_WRITE_TIMEOUT=600 +HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 +HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 + +# Respect X-* headers to redirect clients +RESPECT_XFORWARD_HEADERS_ENABLED=false + +# Log file path +LOG_FILE= +# Log file max size, the unit is MB +LOG_FILE_MAX_SIZE=20 +# Log file max backup count +LOG_FILE_BACKUP_COUNT=5 +# Log dateformat +LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S +# Log Timezone +LOG_TZ=UTC +# Log format +LOG_FORMAT=%(asctime)s,%(msecs)d %(levelname)-2s [%(filename)s:%(lineno)d] %(req_id)s %(message)s + +# Indexing configuration +INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000 + +# Workflow runtime configuration +WORKFLOW_MAX_EXECUTION_STEPS=500 +WORKFLOW_MAX_EXECUTION_TIME=1200 +WORKFLOW_CALL_MAX_DEPTH=5 +WORKFLOW_PARALLEL_DEPTH_LIMIT=3 +MAX_VARIABLE_SIZE=204800 + +# App configuration +APP_MAX_EXECUTION_TIME=1200 +APP_MAX_ACTIVE_REQUESTS=0 + +# Celery beat configuration +CELERY_BEAT_SCHEDULER_TIME=1 + +# Position configuration +POSITION_TOOL_PINS= +POSITION_TOOL_INCLUDES= +POSITION_TOOL_EXCLUDES= + +POSITION_PROVIDER_PINS= +POSITION_PROVIDER_INCLUDES= +POSITION_PROVIDER_EXCLUDES= -# Together API Key -TOGETHER_API_KEY= +# Plugin configuration +PLUGIN_DAEMON_KEY=lYkiYYT6owG+71oLerGzA7GXCgOT++6ovaezWAjpCjf+Sjc3ZtU+qUEi +PLUGIN_DAEMON_URL=http://127.0.0.1:5002 +PLUGIN_REMOTE_INSTALL_PORT=5003 +PLUGIN_REMOTE_INSTALL_HOST=localhost +PLUGIN_MAX_PACKAGE_SIZE=15728640 +INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1 -# Mock Switch -MOCK_SWITCH=false +# Marketplace configuration +MARKETPLACE_ENABLED=true +MARKETPLACE_API_URL=https://marketplace.dify.ai -# CODE EXECUTION CONFIGURATION -CODE_EXECUTION_ENDPOINT= -CODE_EXECUTION_API_KEY= +# Endpoint configuration +ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id} -# Volcengine MaaS Credentials -VOLC_API_KEY= -VOLC_SECRET_KEY= -VOLC_MODEL_ENDPOINT_ID= -VOLC_EMBEDDING_ENDPOINT_ID= +# Reset password token expiry minutes +RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 -# 360 AI Credentials -ZHINAO_API_KEY= +CREATE_TIDB_SERVICE_JOB_ENABLED=false -# Plugin configuration -PLUGIN_DAEMON_KEY= -PLUGIN_DAEMON_URL= +# Maximum number of submitted thread count in a ThreadPool for parallel node execution +MAX_SUBMIT_COUNT=100 +# Lockout duration in seconds +LOGIN_LOCKOUT_DURATION=86400 -# Marketplace configuration -MARKETPLACE_API_URL= -# VESSL AI Credentials -VESSL_AI_MODEL_NAME= -VESSL_AI_API_KEY= -VESSL_AI_ENDPOINT_URL= - -# GPUStack Credentials -GPUSTACK_SERVER_URL= -GPUSTACK_API_KEY= - -# Gitee AI Credentials -GITEE_AI_API_KEY= - -# xAI Credentials -XAI_API_KEY= -XAI_API_BASE= +HTTP_PROXY='http://127.0.0.1:1092' +HTTPS_PROXY='http://127.0.0.1:1092' +NO_PROXY='localhost,127.0.0.1' +LOG_LEVEL=INFO diff --git a/api/tests/integration_tests/conftest.py b/api/tests/integration_tests/conftest.py index 6e3ab4b74b..d9f90f992e 100644 --- a/api/tests/integration_tests/conftest.py +++ b/api/tests/integration_tests/conftest.py @@ -1,19 +1,91 @@ -import os +import pathlib +import random +import secrets +from collections.abc import Generator -# Getting the absolute path of the current file's directory -ABS_PATH = os.path.dirname(os.path.abspath(__file__)) +import pytest +from flask import Flask +from flask.testing import FlaskClient +from sqlalchemy.orm import Session -# Getting the absolute path of the project's root directory -PROJECT_DIR = os.path.abspath(os.path.join(ABS_PATH, os.pardir, os.pardir)) +from app_factory import create_app +from models import Account, DifySetup, Tenant, TenantAccountJoin, db +from services.account_service import AccountService, RegisterService # Loading the .env file if it exists def _load_env() -> None: - dotenv_path = os.path.join(PROJECT_DIR, "tests", "integration_tests", ".env") - if os.path.exists(dotenv_path): + current_file_path = pathlib.Path(__file__).absolute() + # Items later in the list have higher precedence. + files_to_load = [".env", "vdb.env"] + + env_file_paths = [current_file_path.parent / i for i in files_to_load] + for path in env_file_paths: + if not path.exists(): + continue + from dotenv import load_dotenv - load_dotenv(dotenv_path) + # Set `override=True` to ensure values from `vdb.env` take priority over values from `.env` + load_dotenv(str(path), override=True) _load_env() + +_CACHED_APP = create_app() + + +@pytest.fixture +def flask_app() -> Flask: + return _CACHED_APP + + +@pytest.fixture(scope="session") +def setup_account(request) -> Generator[Account, None, None]: + """`dify_setup` completes the setup process for the Dify application. + + It creates `Account` and `Tenant`, and inserts a `DifySetup` record into the database. + + Most tests in the `controllers` package may require dify has been successfully setup. + """ + with _CACHED_APP.test_request_context(): + rand_suffix = random.randint(int(1e6), int(1e7)) # noqa + name = f"test-user-{rand_suffix}" + email = f"{name}@example.com" + RegisterService.setup( + email=email, + name=name, + password=secrets.token_hex(16), + ip_address="localhost", + ) + + with _CACHED_APP.test_request_context(): + with Session(bind=db.engine, expire_on_commit=False) as session: + account = session.query(Account).filter_by(email=email).one() + + yield account + + with _CACHED_APP.test_request_context(): + db.session.query(DifySetup).delete() + db.session.query(TenantAccountJoin).delete() + db.session.query(Account).delete() + db.session.query(Tenant).delete() + db.session.commit() + + +@pytest.fixture +def flask_req_ctx(): + with _CACHED_APP.test_request_context(): + yield + + +@pytest.fixture +def auth_header(setup_account) -> dict[str, str]: + token = AccountService.get_account_jwt_token(setup_account) + return {"Authorization": f"Bearer {token}"} + + +@pytest.fixture +def test_client() -> Generator[FlaskClient, None, None]: + with _CACHED_APP.test_client() as client: + yield client diff --git a/api/tests/integration_tests/controllers/app_fixture.py b/api/tests/integration_tests/controllers/app_fixture.py deleted file mode 100644 index 32e8c11d19..0000000000 --- a/api/tests/integration_tests/controllers/app_fixture.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from app_factory import create_app -from configs import dify_config - -mock_user = type( - "MockUser", - (object,), - { - "is_authenticated": True, - "id": "123", - "is_editor": True, - "is_dataset_editor": True, - "status": "active", - "get_id": "123", - "current_tenant_id": "9d2074fc-6f86-45a9-b09d-6ecc63b9056b", - }, -) - - -@pytest.fixture -def app(): - app = create_app() - dify_config.LOGIN_DISABLED = True - return app diff --git a/api/tests/integration_tests/controllers/console/__init__.py b/api/tests/integration_tests/controllers/console/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/controllers/console/app/__init__.py b/api/tests/integration_tests/controllers/console/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py b/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py new file mode 100644 index 0000000000..038f37af5f --- /dev/null +++ b/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py @@ -0,0 +1,47 @@ +import uuid +from unittest import mock + +from controllers.console.app import workflow_draft_variable as draft_variable_api +from controllers.console.app import wraps +from factories.variable_factory import build_segment +from models import App, AppMode +from models.workflow import WorkflowDraftVariable +from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService + + +def _get_mock_srv_class() -> type[WorkflowDraftVariableService]: + return mock.create_autospec(WorkflowDraftVariableService) + + +class TestWorkflowDraftNodeVariableListApi: + def test_get(self, test_client, auth_header, monkeypatch): + srv_class = _get_mock_srv_class() + mock_app_model: App = App() + mock_app_model.id = str(uuid.uuid4()) + test_node_id = "test_node_id" + mock_app_model.mode = AppMode.ADVANCED_CHAT + mock_load_app_model = mock.Mock(return_value=mock_app_model) + + monkeypatch.setattr(draft_variable_api, "WorkflowDraftVariableService", srv_class) + monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model) + + var1 = WorkflowDraftVariable.new_node_variable( + app_id="test_app_1", + node_id="test_node_1", + name="str_var", + value=build_segment("str_value"), + node_execution_id=str(uuid.uuid4()), + ) + srv_instance = mock.create_autospec(WorkflowDraftVariableService, instance=True) + srv_class.return_value = srv_instance + srv_instance.list_node_variables.return_value = WorkflowDraftVariableList(variables=[var1]) + + response = test_client.get( + f"/console/api/apps/{mock_app_model.id}/workflows/draft/nodes/{test_node_id}/variables", + headers=auth_header, + ) + assert response.status_code == 200 + response_dict = response.json + assert isinstance(response_dict, dict) + assert "items" in response_dict + assert len(response_dict["items"]) == 1 diff --git a/api/tests/integration_tests/controllers/test_controllers.py b/api/tests/integration_tests/controllers/test_controllers.py deleted file mode 100644 index 276ad3a7ed..0000000000 --- a/api/tests/integration_tests/controllers/test_controllers.py +++ /dev/null @@ -1,9 +0,0 @@ -from unittest.mock import patch - -from app_fixture import mock_user # type: ignore - - -def test_post_requires_login(app): - with app.test_client() as client, patch("flask_login.utils._get_user", mock_user): - response = client.get("/console/api/data-source/integrates") - assert response.status_code == 200 diff --git a/api/tests/integration_tests/factories/__init__.py b/api/tests/integration_tests/factories/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/factories/test_storage_key_loader.py b/api/tests/integration_tests/factories/test_storage_key_loader.py new file mode 100644 index 0000000000..fecb3f6d95 --- /dev/null +++ b/api/tests/integration_tests/factories/test_storage_key_loader.py @@ -0,0 +1,371 @@ +import unittest +from datetime import UTC, datetime +from typing import Optional +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from sqlalchemy.orm import Session + +from core.file import File, FileTransferMethod, FileType +from extensions.ext_database import db +from factories.file_factory import StorageKeyLoader +from models import ToolFile, UploadFile +from models.enums import CreatorUserRole + + +@pytest.mark.usefixtures("flask_req_ctx") +class TestStorageKeyLoader(unittest.TestCase): + """ + Integration tests for StorageKeyLoader class. + + Tests the batched loading of storage keys from the database for files + with different transfer methods: LOCAL_FILE, REMOTE_URL, and TOOL_FILE. + """ + + def setUp(self): + """Set up test data before each test method.""" + self.session = db.session() + self.tenant_id = str(uuid4()) + self.user_id = str(uuid4()) + self.conversation_id = str(uuid4()) + + # Create test data that will be cleaned up after each test + self.test_upload_files = [] + self.test_tool_files = [] + + # Create StorageKeyLoader instance + self.loader = StorageKeyLoader(self.session, self.tenant_id) + + def tearDown(self): + """Clean up test data after each test method.""" + self.session.rollback() + + def _create_upload_file( + self, file_id: Optional[str] = None, storage_key: Optional[str] = None, tenant_id: Optional[str] = None + ) -> UploadFile: + """Helper method to create an UploadFile record for testing.""" + if file_id is None: + file_id = str(uuid4()) + if storage_key is None: + storage_key = f"test_storage_key_{uuid4()}" + if tenant_id is None: + tenant_id = self.tenant_id + + upload_file = UploadFile( + tenant_id=tenant_id, + storage_type="local", + key=storage_key, + name="test_file.txt", + size=1024, + extension=".txt", + mime_type="text/plain", + created_by_role=CreatorUserRole.ACCOUNT, + created_by=self.user_id, + created_at=datetime.now(UTC), + used=False, + ) + upload_file.id = file_id + + self.session.add(upload_file) + self.session.flush() + self.test_upload_files.append(upload_file) + + return upload_file + + def _create_tool_file( + self, file_id: Optional[str] = None, file_key: Optional[str] = None, tenant_id: Optional[str] = None + ) -> ToolFile: + """Helper method to create a ToolFile record for testing.""" + if file_id is None: + file_id = str(uuid4()) + if file_key is None: + file_key = f"test_file_key_{uuid4()}" + if tenant_id is None: + tenant_id = self.tenant_id + + tool_file = ToolFile() + tool_file.id = file_id + tool_file.user_id = self.user_id + tool_file.tenant_id = tenant_id + tool_file.conversation_id = self.conversation_id + tool_file.file_key = file_key + tool_file.mimetype = "text/plain" + tool_file.original_url = "http://example.com/file.txt" + tool_file.name = "test_tool_file.txt" + tool_file.size = 2048 + + self.session.add(tool_file) + self.session.flush() + self.test_tool_files.append(tool_file) + + return tool_file + + def _create_file( + self, related_id: str, transfer_method: FileTransferMethod, tenant_id: Optional[str] = None + ) -> File: + """Helper method to create a File object for testing.""" + if tenant_id is None: + tenant_id = self.tenant_id + + # Set related_id for LOCAL_FILE and TOOL_FILE transfer methods + file_related_id = None + remote_url = None + + if transfer_method in (FileTransferMethod.LOCAL_FILE, FileTransferMethod.TOOL_FILE): + file_related_id = related_id + elif transfer_method == FileTransferMethod.REMOTE_URL: + remote_url = "https://example.com/test_file.txt" + file_related_id = related_id + + return File( + id=str(uuid4()), # Generate new UUID for File.id + tenant_id=tenant_id, + type=FileType.DOCUMENT, + transfer_method=transfer_method, + related_id=file_related_id, + remote_url=remote_url, + filename="test_file.txt", + extension=".txt", + mime_type="text/plain", + size=1024, + storage_key="initial_key", + ) + + def test_load_storage_keys_local_file(self): + """Test loading storage keys for LOCAL_FILE transfer method.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == upload_file.key + + def test_load_storage_keys_remote_url(self): + """Test loading storage keys for REMOTE_URL transfer method.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.REMOTE_URL) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == upload_file.key + + def test_load_storage_keys_tool_file(self): + """Test loading storage keys for TOOL_FILE transfer method.""" + # Create test data + tool_file = self._create_tool_file() + file = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) + + # Load storage keys + self.loader.load_storage_keys([file]) + + # Verify storage key was loaded correctly + assert file._storage_key == tool_file.file_key + + def test_load_storage_keys_mixed_methods(self): + """Test batch loading with mixed transfer methods.""" + # Create test data for different transfer methods + upload_file1 = self._create_upload_file() + upload_file2 = self._create_upload_file() + tool_file = self._create_tool_file() + + file1 = self._create_file(related_id=upload_file1.id, transfer_method=FileTransferMethod.LOCAL_FILE) + file2 = self._create_file(related_id=upload_file2.id, transfer_method=FileTransferMethod.REMOTE_URL) + file3 = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) + + files = [file1, file2, file3] + + # Load storage keys + self.loader.load_storage_keys(files) + + # Verify all storage keys were loaded correctly + assert file1._storage_key == upload_file1.key + assert file2._storage_key == upload_file2.key + assert file3._storage_key == tool_file.file_key + + def test_load_storage_keys_empty_list(self): + """Test with empty file list.""" + # Should not raise any exceptions + self.loader.load_storage_keys([]) + + def test_load_storage_keys_tenant_mismatch(self): + """Test tenant_id validation.""" + # Create file with different tenant_id + upload_file = self._create_upload_file() + file = self._create_file( + related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=str(uuid4()) + ) + + # Should raise ValueError for tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file]) + + assert "invalid file, expected tenant_id" in str(context.value) + + def test_load_storage_keys_missing_file_id(self): + """Test with None file.related_id.""" + # Create a file with valid parameters first, then manually set related_id to None + file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) + file.related_id = None + + # Should raise ValueError for None file related_id + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file]) + + assert str(context.value) == "file id should not be None." + + def test_load_storage_keys_nonexistent_upload_file_records(self): + """Test with missing UploadFile database records.""" + # Create file with non-existent upload file id + non_existent_id = str(uuid4()) + file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Should raise ValueError for missing record + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_nonexistent_tool_file_records(self): + """Test with missing ToolFile database records.""" + # Create file with non-existent tool file id + non_existent_id = str(uuid4()) + file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.TOOL_FILE) + + # Should raise ValueError for missing record + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_invalid_uuid(self): + """Test with invalid UUID format.""" + # Create a file with valid parameters first, then manually set invalid related_id + file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) + file.related_id = "invalid-uuid-format" + + # Should raise ValueError for invalid UUID + with pytest.raises(ValueError): + self.loader.load_storage_keys([file]) + + def test_load_storage_keys_batch_efficiency(self): + """Test batched operations use efficient queries.""" + # Create multiple files of different types + upload_files = [self._create_upload_file() for _ in range(3)] + tool_files = [self._create_tool_file() for _ in range(2)] + + files = [] + files.extend( + [self._create_file(related_id=uf.id, transfer_method=FileTransferMethod.LOCAL_FILE) for uf in upload_files] + ) + files.extend( + [self._create_file(related_id=tf.id, transfer_method=FileTransferMethod.TOOL_FILE) for tf in tool_files] + ) + + # Mock the session to count queries + with patch.object(self.session, "scalars", wraps=self.session.scalars) as mock_scalars: + self.loader.load_storage_keys(files) + + # Should make exactly 2 queries (one for upload_files, one for tool_files) + assert mock_scalars.call_count == 2 + + # Verify all storage keys were loaded correctly + for i, file in enumerate(files[:3]): + assert file._storage_key == upload_files[i].key + for i, file in enumerate(files[3:]): + assert file._storage_key == tool_files[i].file_key + + def test_load_storage_keys_tenant_isolation(self): + """Test that tenant isolation works correctly.""" + # Create files for different tenants + other_tenant_id = str(uuid4()) + + # Create upload file for current tenant + upload_file_current = self._create_upload_file() + file_current = self._create_file( + related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE + ) + + # Create upload file for other tenant (but don't add to cleanup list) + upload_file_other = UploadFile( + tenant_id=other_tenant_id, + storage_type="local", + key="other_tenant_key", + name="other_file.txt", + size=1024, + extension=".txt", + mime_type="text/plain", + created_by_role=CreatorUserRole.ACCOUNT, + created_by=self.user_id, + created_at=datetime.now(UTC), + used=False, + ) + upload_file_other.id = str(uuid4()) + self.session.add(upload_file_other) + self.session.flush() + + # Create file for other tenant but try to load with current tenant's loader + file_other = self._create_file( + related_id=upload_file_other.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id + ) + + # Should raise ValueError due to tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file_other]) + + assert "invalid file, expected tenant_id" in str(context.value) + + # Current tenant's file should still work + self.loader.load_storage_keys([file_current]) + assert file_current._storage_key == upload_file_current.key + + def test_load_storage_keys_mixed_tenant_batch(self): + """Test batch with mixed tenant files (should fail on first mismatch).""" + # Create files for current tenant + upload_file_current = self._create_upload_file() + file_current = self._create_file( + related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE + ) + + # Create file for different tenant + other_tenant_id = str(uuid4()) + file_other = self._create_file( + related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id + ) + + # Should raise ValueError on tenant mismatch + with pytest.raises(ValueError) as context: + self.loader.load_storage_keys([file_current, file_other]) + + assert "invalid file, expected tenant_id" in str(context.value) + + def test_load_storage_keys_duplicate_file_ids(self): + """Test handling of duplicate file IDs in the batch.""" + # Create upload file + upload_file = self._create_upload_file() + + # Create two File objects with same related_id + file1 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + file2 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Should handle duplicates gracefully + self.loader.load_storage_keys([file1, file2]) + + # Both files should have the same storage key + assert file1._storage_key == upload_file.key + assert file2._storage_key == upload_file.key + + def test_load_storage_keys_session_isolation(self): + """Test that the loader uses the provided session correctly.""" + # Create test data + upload_file = self._create_upload_file() + file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) + + # Create loader with different session (same underlying connection) + + with Session(bind=db.engine) as other_session: + other_loader = StorageKeyLoader(other_session, self.tenant_id) + with pytest.raises(ValueError): + other_loader.load_storage_keys([file]) diff --git a/api/tests/integration_tests/services/__init__.py b/api/tests/integration_tests/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/services/test_workflow_draft_variable_service.py b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py new file mode 100644 index 0000000000..30cd2e60cb --- /dev/null +++ b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py @@ -0,0 +1,501 @@ +import json +import unittest +import uuid + +import pytest +from sqlalchemy.orm import Session + +from core.variables.variables import StringVariable +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from core.workflow.nodes import NodeType +from factories.variable_factory import build_segment +from libs import datetime_utils +from models import db +from models.workflow import Workflow, WorkflowDraftVariable, WorkflowNodeExecutionModel +from services.workflow_draft_variable_service import DraftVarLoader, VariableResetError, WorkflowDraftVariableService + + +@pytest.mark.usefixtures("flask_req_ctx") +class TestWorkflowDraftVariableService(unittest.TestCase): + _test_app_id: str + _session: Session + _node1_id = "test_node_1" + _node2_id = "test_node_2" + _node_exec_id = str(uuid.uuid4()) + + def setUp(self): + self._test_app_id = str(uuid.uuid4()) + self._session: Session = db.session() + sys_var = WorkflowDraftVariable.new_sys_variable( + app_id=self._test_app_id, + name="sys_var", + value=build_segment("sys_value"), + node_execution_id=self._node_exec_id, + ) + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=self._test_app_id, + name="conv_var", + value=build_segment("conv_value"), + ) + node2_vars = [ + WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node2_id, + name="int_var", + value=build_segment(1), + visible=False, + node_execution_id=self._node_exec_id, + ), + WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node2_id, + name="str_var", + value=build_segment("str_value"), + visible=True, + node_execution_id=self._node_exec_id, + ), + ] + node1_var = WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node1_id, + name="str_var", + value=build_segment("str_value"), + visible=True, + node_execution_id=self._node_exec_id, + ) + _variables = list(node2_vars) + _variables.extend( + [ + node1_var, + sys_var, + conv_var, + ] + ) + + db.session.add_all(_variables) + db.session.flush() + self._variable_ids = [v.id for v in _variables] + self._node1_str_var_id = node1_var.id + self._sys_var_id = sys_var.id + self._conv_var_id = conv_var.id + self._node2_var_ids = [v.id for v in node2_vars] + + def _get_test_srv(self) -> WorkflowDraftVariableService: + return WorkflowDraftVariableService(session=self._session) + + def tearDown(self): + self._session.rollback() + + def test_list_variables(self): + srv = self._get_test_srv() + var_list = srv.list_variables_without_values(self._test_app_id, page=1, limit=2) + assert var_list.total == 5 + assert len(var_list.variables) == 2 + page1_var_ids = {v.id for v in var_list.variables} + assert page1_var_ids.issubset(self._variable_ids) + + var_list_2 = srv.list_variables_without_values(self._test_app_id, page=2, limit=2) + assert var_list_2.total is None + assert len(var_list_2.variables) == 2 + page2_var_ids = {v.id for v in var_list_2.variables} + assert page2_var_ids.isdisjoint(page1_var_ids) + assert page2_var_ids.issubset(self._variable_ids) + + def test_get_node_variable(self): + srv = self._get_test_srv() + node_var = srv.get_node_variable(self._test_app_id, self._node1_id, "str_var") + assert node_var is not None + assert node_var.id == self._node1_str_var_id + assert node_var.name == "str_var" + assert node_var.get_value() == build_segment("str_value") + + def test_get_system_variable(self): + srv = self._get_test_srv() + sys_var = srv.get_system_variable(self._test_app_id, "sys_var") + assert sys_var is not None + assert sys_var.id == self._sys_var_id + assert sys_var.name == "sys_var" + assert sys_var.get_value() == build_segment("sys_value") + + def test_get_conversation_variable(self): + srv = self._get_test_srv() + conv_var = srv.get_conversation_variable(self._test_app_id, "conv_var") + assert conv_var is not None + assert conv_var.id == self._conv_var_id + assert conv_var.name == "conv_var" + assert conv_var.get_value() == build_segment("conv_value") + + def test_delete_node_variables(self): + srv = self._get_test_srv() + srv.delete_node_variables(self._test_app_id, self._node2_id) + node2_var_count = ( + self._session.query(WorkflowDraftVariable) + .where( + WorkflowDraftVariable.app_id == self._test_app_id, + WorkflowDraftVariable.node_id == self._node2_id, + ) + .count() + ) + assert node2_var_count == 0 + + def test_delete_variable(self): + srv = self._get_test_srv() + node_1_var = ( + self._session.query(WorkflowDraftVariable).where(WorkflowDraftVariable.id == self._node1_str_var_id).one() + ) + srv.delete_variable(node_1_var) + exists = bool( + self._session.query(WorkflowDraftVariable).where(WorkflowDraftVariable.id == self._node1_str_var_id).first() + ) + assert exists is False + + def test__list_node_variables(self): + srv = self._get_test_srv() + node_vars = srv._list_node_variables(self._test_app_id, self._node2_id) + assert len(node_vars.variables) == 2 + assert {v.id for v in node_vars.variables} == set(self._node2_var_ids) + + def test_get_draft_variables_by_selectors(self): + srv = self._get_test_srv() + selectors = [ + [self._node1_id, "str_var"], + [self._node2_id, "str_var"], + [self._node2_id, "int_var"], + ] + variables = srv.get_draft_variables_by_selectors(self._test_app_id, selectors) + assert len(variables) == 3 + assert {v.id for v in variables} == {self._node1_str_var_id} | set(self._node2_var_ids) + + +@pytest.mark.usefixtures("flask_req_ctx") +class TestDraftVariableLoader(unittest.TestCase): + _test_app_id: str + _test_tenant_id: str + + _node1_id = "test_loader_node_1" + _node_exec_id = str(uuid.uuid4()) + + def setUp(self): + self._test_app_id = str(uuid.uuid4()) + self._test_tenant_id = str(uuid.uuid4()) + sys_var = WorkflowDraftVariable.new_sys_variable( + app_id=self._test_app_id, + name="sys_var", + value=build_segment("sys_value"), + node_execution_id=self._node_exec_id, + ) + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=self._test_app_id, + name="conv_var", + value=build_segment("conv_value"), + ) + node_var = WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node1_id, + name="str_var", + value=build_segment("str_value"), + visible=True, + node_execution_id=self._node_exec_id, + ) + _variables = [ + node_var, + sys_var, + conv_var, + ] + + with Session(bind=db.engine, expire_on_commit=False) as session: + session.add_all(_variables) + session.flush() + session.commit() + self._variable_ids = [v.id for v in _variables] + self._node_var_id = node_var.id + self._sys_var_id = sys_var.id + self._conv_var_id = conv_var.id + + def tearDown(self): + with Session(bind=db.engine, expire_on_commit=False) as session: + session.query(WorkflowDraftVariable).filter(WorkflowDraftVariable.app_id == self._test_app_id).delete( + synchronize_session=False + ) + session.commit() + + def test_variable_loader_with_empty_selector(self): + var_loader = DraftVarLoader(engine=db.engine, app_id=self._test_app_id, tenant_id=self._test_tenant_id) + variables = var_loader.load_variables([]) + assert len(variables) == 0 + + def test_variable_loader_with_non_empty_selector(self): + var_loader = DraftVarLoader(engine=db.engine, app_id=self._test_app_id, tenant_id=self._test_tenant_id) + variables = var_loader.load_variables( + [ + [SYSTEM_VARIABLE_NODE_ID, "sys_var"], + [CONVERSATION_VARIABLE_NODE_ID, "conv_var"], + [self._node1_id, "str_var"], + ] + ) + assert len(variables) == 3 + conv_var = next(v for v in variables if v.selector[0] == CONVERSATION_VARIABLE_NODE_ID) + assert conv_var.id == self._conv_var_id + sys_var = next(v for v in variables if v.selector[0] == SYSTEM_VARIABLE_NODE_ID) + assert sys_var.id == self._sys_var_id + node1_var = next(v for v in variables if v.selector[0] == self._node1_id) + assert node1_var.id == self._node_var_id + + +@pytest.mark.usefixtures("flask_req_ctx") +class TestWorkflowDraftVariableServiceResetVariable(unittest.TestCase): + """Integration tests for reset_variable functionality using real database""" + + _test_app_id: str + _test_tenant_id: str + _test_workflow_id: str + _session: Session + _node_id = "test_reset_node" + _node_exec_id: str + _workflow_node_exec_id: str + + def setUp(self): + self._test_app_id = str(uuid.uuid4()) + self._test_tenant_id = str(uuid.uuid4()) + self._test_workflow_id = str(uuid.uuid4()) + self._node_exec_id = str(uuid.uuid4()) + self._workflow_node_exec_id = str(uuid.uuid4()) + self._session: Session = db.session() + + # Create a workflow node execution record with outputs + # Note: The WorkflowNodeExecutionModel.id should match the node_execution_id in WorkflowDraftVariable + self._workflow_node_execution = WorkflowNodeExecutionModel( + id=self._node_exec_id, # This should match the node_execution_id in the variable + tenant_id=self._test_tenant_id, + app_id=self._test_app_id, + workflow_id=self._test_workflow_id, + triggered_from="workflow-run", + workflow_run_id=str(uuid.uuid4()), + index=1, + node_execution_id=self._node_exec_id, + node_id=self._node_id, + node_type=NodeType.LLM.value, + title="Test Node", + inputs='{"input": "test input"}', + process_data='{"test_var": "process_value", "other_var": "other_process"}', + outputs='{"test_var": "output_value", "other_var": "other_output"}', + status="succeeded", + elapsed_time=1.5, + created_by_role="account", + created_by=str(uuid.uuid4()), + ) + + # Create conversation variables for the workflow + self._conv_variables = [ + StringVariable( + id=str(uuid.uuid4()), + name="conv_var_1", + description="Test conversation variable 1", + value="default_value_1", + ), + StringVariable( + id=str(uuid.uuid4()), + name="conv_var_2", + description="Test conversation variable 2", + value="default_value_2", + ), + ] + + # Create test variables + self._node_var_with_exec = WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node_id, + name="test_var", + value=build_segment("old_value"), + node_execution_id=self._node_exec_id, + ) + self._node_var_with_exec.last_edited_at = datetime_utils.naive_utc_now() + + self._node_var_without_exec = WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node_id, + name="no_exec_var", + value=build_segment("some_value"), + node_execution_id="temp_exec_id", + ) + # Manually set node_execution_id to None after creation + self._node_var_without_exec.node_execution_id = None + + self._node_var_missing_exec = WorkflowDraftVariable.new_node_variable( + app_id=self._test_app_id, + node_id=self._node_id, + name="missing_exec_var", + value=build_segment("some_value"), + node_execution_id=str(uuid.uuid4()), # Use a valid UUID that doesn't exist in database + ) + + self._conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=self._test_app_id, + name="conv_var_1", + value=build_segment("old_conv_value"), + ) + self._conv_var.last_edited_at = datetime_utils.naive_utc_now() + + # Add all to database + db.session.add_all( + [ + self._workflow_node_execution, + self._node_var_with_exec, + self._node_var_without_exec, + self._node_var_missing_exec, + self._conv_var, + ] + ) + db.session.flush() + + # Store IDs for assertions + self._node_var_with_exec_id = self._node_var_with_exec.id + self._node_var_without_exec_id = self._node_var_without_exec.id + self._node_var_missing_exec_id = self._node_var_missing_exec.id + self._conv_var_id = self._conv_var.id + + def _get_test_srv(self) -> WorkflowDraftVariableService: + return WorkflowDraftVariableService(session=self._session) + + def _create_mock_workflow(self) -> Workflow: + """Create a real workflow with conversation variables and graph""" + conversation_vars = self._conv_variables + + # Create a simple graph with the test node + graph = { + "nodes": [{"id": "test_reset_node", "type": "llm", "title": "Test Node", "data": {"type": "llm"}}], + "edges": [], + } + + workflow = Workflow.new( + tenant_id=str(uuid.uuid4()), + app_id=self._test_app_id, + type="workflow", + version="1.0", + graph=json.dumps(graph), + features="{}", + created_by=str(uuid.uuid4()), + environment_variables=[], + conversation_variables=conversation_vars, + ) + return workflow + + def tearDown(self): + self._session.rollback() + + def test_reset_node_variable_with_valid_execution_record(self): + """Test resetting a node variable with valid execution record - should restore from execution""" + srv = self._get_test_srv() + mock_workflow = self._create_mock_workflow() + + # Get the variable before reset + variable = srv.get_variable(self._node_var_with_exec_id) + assert variable is not None + assert variable.get_value().value == "old_value" + assert variable.last_edited_at is not None + + # Reset the variable + result = srv.reset_variable(mock_workflow, variable) + + # Should return the updated variable + assert result is not None + assert result.id == self._node_var_with_exec_id + assert result.node_execution_id == self._workflow_node_execution.id + assert result.last_edited_at is None # Should be reset to None + + # The returned variable should have the updated value from execution record + assert result.get_value().value == "output_value" + + # Verify the variable was updated in database + updated_variable = srv.get_variable(self._node_var_with_exec_id) + assert updated_variable is not None + # The value should be updated from the execution record's outputs + assert updated_variable.get_value().value == "output_value" + assert updated_variable.last_edited_at is None + assert updated_variable.node_execution_id == self._workflow_node_execution.id + + def test_reset_node_variable_with_no_execution_id(self): + """Test resetting a node variable with no execution ID - should delete variable""" + srv = self._get_test_srv() + mock_workflow = self._create_mock_workflow() + + # Get the variable before reset + variable = srv.get_variable(self._node_var_without_exec_id) + assert variable is not None + + # Reset the variable + result = srv.reset_variable(mock_workflow, variable) + + # Should return None (variable deleted) + assert result is None + + # Verify the variable was deleted + deleted_variable = srv.get_variable(self._node_var_without_exec_id) + assert deleted_variable is None + + def test_reset_node_variable_with_missing_execution_record(self): + """Test resetting a node variable when execution record doesn't exist""" + srv = self._get_test_srv() + mock_workflow = self._create_mock_workflow() + + # Get the variable before reset + variable = srv.get_variable(self._node_var_missing_exec_id) + assert variable is not None + + # Reset the variable + result = srv.reset_variable(mock_workflow, variable) + + # Should return None (variable deleted) + assert result is None + + # Verify the variable was deleted + deleted_variable = srv.get_variable(self._node_var_missing_exec_id) + assert deleted_variable is None + + def test_reset_conversation_variable(self): + """Test resetting a conversation variable""" + srv = self._get_test_srv() + mock_workflow = self._create_mock_workflow() + + # Get the variable before reset + variable = srv.get_variable(self._conv_var_id) + assert variable is not None + assert variable.get_value().value == "old_conv_value" + assert variable.last_edited_at is not None + + # Reset the variable + result = srv.reset_variable(mock_workflow, variable) + + # Should return the updated variable + assert result is not None + assert result.id == self._conv_var_id + assert result.last_edited_at is None # Should be reset to None + + # Verify the variable was updated with default value from workflow + updated_variable = srv.get_variable(self._conv_var_id) + assert updated_variable is not None + # The value should be updated from the workflow's conversation variable default + assert updated_variable.get_value().value == "default_value_1" + assert updated_variable.last_edited_at is None + + def test_reset_system_variable_raises_error(self): + """Test that resetting a system variable raises an error""" + srv = self._get_test_srv() + mock_workflow = self._create_mock_workflow() + + # Create a system variable + sys_var = WorkflowDraftVariable.new_sys_variable( + app_id=self._test_app_id, + name="sys_var", + value=build_segment("sys_value"), + node_execution_id=self._node_exec_id, + ) + db.session.add(sys_var) + db.session.flush() + + # Attempt to reset the system variable + with pytest.raises(VariableResetError) as exc_info: + srv.reset_variable(mock_workflow, sys_var) + + assert "cannot reset system variable" in str(exc_info.value) + assert sys_var.id in str(exc_info.value) diff --git a/api/tests/integration_tests/vdb/couchbase/test_couchbase.py b/api/tests/integration_tests/vdb/couchbase/test_couchbase.py index d76c34ba0e..eef1ee4e75 100644 --- a/api/tests/integration_tests/vdb/couchbase/test_couchbase.py +++ b/api/tests/integration_tests/vdb/couchbase/test_couchbase.py @@ -4,7 +4,6 @@ import time from core.rag.datasource.vdb.couchbase.couchbase_vector import CouchbaseConfig, CouchbaseVector from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, - get_example_text, setup_mock_redis, ) diff --git a/api/tests/integration_tests/vdb/matrixone/__init__.py b/api/tests/integration_tests/vdb/matrixone/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/integration_tests/vdb/matrixone/test_matrixone.py b/api/tests/integration_tests/vdb/matrixone/test_matrixone.py new file mode 100644 index 0000000000..c4056db63e --- /dev/null +++ b/api/tests/integration_tests/vdb/matrixone/test_matrixone.py @@ -0,0 +1,24 @@ +from core.rag.datasource.vdb.matrixone.matrixone_vector import MatrixoneConfig, MatrixoneVector +from tests.integration_tests.vdb.test_vector_store import ( + AbstractVectorTest, + setup_mock_redis, +) + + +class MatrixoneVectorTest(AbstractVectorTest): + def __init__(self): + super().__init__() + self.vector = MatrixoneVector( + collection_name=self.collection_name, + config=MatrixoneConfig( + host="localhost", port=6001, user="dump", password="111", database="dify", metric="l2" + ), + ) + + def get_ids_by_metadata_field(self): + ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id) + assert len(ids) == 1 + + +def test_matrixone_vector(setup_mock_redis): + MatrixoneVectorTest().run_all_tests() diff --git a/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py b/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py index ebcb134168..8fbbbe61b8 100644 --- a/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py +++ b/api/tests/integration_tests/vdb/oceanbase/test_oceanbase.py @@ -1,15 +1,11 @@ -from unittest.mock import MagicMock, patch - import pytest from core.rag.datasource.vdb.oceanbase.oceanbase_vector import ( OceanBaseVector, OceanBaseVectorConfig, ) -from tests.integration_tests.vdb.__mock.tcvectordb import setup_tcvectordb_mock from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, - get_example_text, setup_mock_redis, ) @@ -20,10 +16,11 @@ def oceanbase_vector(): "dify_test_collection", config=OceanBaseVectorConfig( host="127.0.0.1", - port="2881", - user="root@test", + port=2881, + user="root", database="test", - password="test", + password="difyai123456", + enable_hybrid_search=True, ), ) @@ -33,39 +30,13 @@ class OceanBaseVectorTest(AbstractVectorTest): super().__init__() self.vector = vector - def search_by_vector(self): - hits_by_vector = self.vector.search_by_vector(query_vector=self.example_embedding) - assert len(hits_by_vector) == 0 - - def search_by_full_text(self): - hits_by_full_text = self.vector.search_by_full_text(query=get_example_text()) - assert len(hits_by_full_text) == 0 - - def text_exists(self): - exist = self.vector.text_exists(self.example_doc_id) - assert exist == True - def get_ids_by_metadata_field(self): ids = self.vector.get_ids_by_metadata_field(key="document_id", value=self.example_doc_id) - assert len(ids) == 0 - - -@pytest.fixture -def setup_mock_oceanbase_client(): - with patch("core.rag.datasource.vdb.oceanbase.oceanbase_vector.ObVecClient", new_callable=MagicMock) as mock_client: - yield mock_client - - -@pytest.fixture -def setup_mock_oceanbase_vector(oceanbase_vector): - with patch.object(oceanbase_vector, "_client"): - yield oceanbase_vector + assert len(ids) == 1 def test_oceanbase_vector( setup_mock_redis, - setup_mock_oceanbase_client, - setup_mock_oceanbase_vector, oceanbase_vector, ): OceanBaseVectorTest(oceanbase_vector).run_all_tests() diff --git a/api/tests/integration_tests/vdb/opengauss/test_opengauss.py b/api/tests/integration_tests/vdb/opengauss/test_opengauss.py index f2013848bf..2a1129493c 100644 --- a/api/tests/integration_tests/vdb/opengauss/test_opengauss.py +++ b/api/tests/integration_tests/vdb/opengauss/test_opengauss.py @@ -5,7 +5,6 @@ import psycopg2 # type: ignore from core.rag.datasource.vdb.opengauss.opengauss import OpenGauss, OpenGaussConfig from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, - get_example_text, setup_mock_redis, ) diff --git a/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py b/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py index 3d7873442b..02931fef5a 100644 --- a/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py +++ b/api/tests/integration_tests/vdb/pyvastbase/test_vastbase_vector.py @@ -1,7 +1,6 @@ from core.rag.datasource.vdb.pyvastbase.vastbase_vector import VastbaseVector, VastbaseVectorConfig from tests.integration_tests.vdb.test_vector_store import ( AbstractVectorTest, - get_example_text, setup_mock_redis, ) diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index 13d78c2d83..90bb04f649 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -9,12 +9,12 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.code.code_node import CodeNode from core.workflow.nodes.code.entities import CodeNodeData +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock @@ -50,7 +50,7 @@ def init_code_node(code_config: dict): # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], conversation_variables=[], diff --git a/api/tests/integration_tests/workflow/nodes/test_http.py b/api/tests/integration_tests/workflow/nodes/test_http.py index 1ab0cc2451..50e726febf 100644 --- a/api/tests/integration_tests/workflow/nodes/test_http.py +++ b/api/tests/integration_tests/workflow/nodes/test_http.py @@ -6,11 +6,11 @@ import pytest from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.http_request.node import HttpRequestNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType from tests.integration_tests.workflow.nodes.__mock.http import setup_http_mock @@ -44,7 +44,7 @@ def init_http_node(config: dict): # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], conversation_variables=[], diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index 6aa48b1cbb..ff119b7482 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -1,5 +1,4 @@ import json -import os import time import uuid from collections.abc import Generator @@ -8,19 +7,18 @@ from unittest.mock import MagicMock, patch import pytest -from app_factory import create_app -from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom +from core.llm_generator.output_parser.structured_output import _parse_structured_output from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage from core.model_runtime.entities.message_entities import AssistantPromptMessage from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.event import RunCompletedEvent from core.workflow.nodes.llm.node import LLMNode +from core.workflow.system_variable import SystemVariable from extensions.ext_database import db from models.enums import UserFrom from models.workflow import WorkflowType @@ -30,21 +28,6 @@ from tests.integration_tests.model_runtime.__mock.plugin_daemon import setup_mod from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock -@pytest.fixture(scope="session") -def app(): - # Set up storage configuration - os.environ["STORAGE_TYPE"] = "opendal" - os.environ["OPENDAL_SCHEME"] = "fs" - os.environ["OPENDAL_FS_ROOT"] = "storage" - - # Ensure storage directory exists - os.makedirs("storage", exist_ok=True) - - app = create_app() - dify_config.LOGIN_DISABLED = True - return app - - def init_llm_node(config: dict) -> LLMNode: graph_config = { "edges": [ @@ -79,12 +62,14 @@ def init_llm_node(config: dict) -> LLMNode: # construct variable pool variable_pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "what's the weather today?", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, + system_variables=SystemVariable( + user_id="aaa", + app_id=app_id, + workflow_id=workflow_id, + files=[], + query="what's the weather today?", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], conversation_variables=[], @@ -102,200 +87,99 @@ def init_llm_node(config: dict) -> LLMNode: return node -def test_execute_llm(app): - with app.app_context(): - node = init_llm_node( - config={ - "id": "llm", - "data": { - "title": "123", - "type": "llm", - "model": { - "provider": "langgenius/openai/openai", - "name": "gpt-3.5-turbo", - "mode": "chat", - "completion_params": {}, - }, - "prompt_template": [ - { - "role": "system", - "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}.", - }, - {"role": "user", "text": "{{#sys.query#}}"}, - ], - "memory": None, - "context": {"enabled": False}, - "vision": {"enabled": False}, +def test_execute_llm(flask_req_ctx): + node = init_llm_node( + config={ + "id": "llm", + "data": { + "title": "123", + "type": "llm", + "model": { + "provider": "langgenius/openai/openai", + "name": "gpt-3.5-turbo", + "mode": "chat", + "completion_params": {}, }, + "prompt_template": [ + { + "role": "system", + "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}.", + }, + {"role": "user", "text": "{{#sys.query#}}"}, + ], + "memory": None, + "context": {"enabled": False}, + "vision": {"enabled": False}, }, - ) - - credentials = {"openai_api_key": os.environ.get("OPENAI_API_KEY")} - - # Create a proper LLM result with real entities - mock_usage = LLMUsage( - prompt_tokens=30, - prompt_unit_price=Decimal("0.001"), - prompt_price_unit=Decimal("1000"), - prompt_price=Decimal("0.00003"), - completion_tokens=20, - completion_unit_price=Decimal("0.002"), - completion_price_unit=Decimal("1000"), - completion_price=Decimal("0.00004"), - total_tokens=50, - total_price=Decimal("0.00007"), - currency="USD", - latency=0.5, - ) - - mock_message = AssistantPromptMessage(content="This is a test response from the mocked LLM.") - - mock_llm_result = LLMResult( - model="gpt-3.5-turbo", - prompt_messages=[], - message=mock_message, - usage=mock_usage, - ) - - # Create a simple mock model instance that doesn't call real providers - mock_model_instance = MagicMock() - mock_model_instance.invoke_llm.return_value = mock_llm_result - - # Create a simple mock model config with required attributes - mock_model_config = MagicMock() - mock_model_config.mode = "chat" - mock_model_config.provider = "langgenius/openai/openai" - mock_model_config.model = "gpt-3.5-turbo" - mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b" - - # Mock the _fetch_model_config method - def mock_fetch_model_config_func(_node_data_model): - return mock_model_instance, mock_model_config - - # Also mock ModelManager.get_model_instance to avoid database calls - def mock_get_model_instance(_self, **kwargs): - return mock_model_instance - - with ( - patch.object(node, "_fetch_model_config", mock_fetch_model_config_func), - patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance), - ): - # execute node - result = node._run() - assert isinstance(result, Generator) - - for item in result: - if isinstance(item, RunCompletedEvent): - assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.process_data is not None - assert item.run_result.outputs is not None - assert item.run_result.outputs.get("text") is not None - assert item.run_result.outputs.get("usage", {})["total_tokens"] > 0 + }, + ) + + # Create a proper LLM result with real entities + mock_usage = LLMUsage( + prompt_tokens=30, + prompt_unit_price=Decimal("0.001"), + prompt_price_unit=Decimal(1000), + prompt_price=Decimal("0.00003"), + completion_tokens=20, + completion_unit_price=Decimal("0.002"), + completion_price_unit=Decimal(1000), + completion_price=Decimal("0.00004"), + total_tokens=50, + total_price=Decimal("0.00007"), + currency="USD", + latency=0.5, + ) + + mock_message = AssistantPromptMessage(content="This is a test response from the mocked LLM.") + + mock_llm_result = LLMResult( + model="gpt-3.5-turbo", + prompt_messages=[], + message=mock_message, + usage=mock_usage, + ) + + # Create a simple mock model instance that doesn't call real providers + mock_model_instance = MagicMock() + mock_model_instance.invoke_llm.return_value = mock_llm_result + + # Create a simple mock model config with required attributes + mock_model_config = MagicMock() + mock_model_config.mode = "chat" + mock_model_config.provider = "langgenius/openai/openai" + mock_model_config.model = "gpt-3.5-turbo" + mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b" + + # Mock the _fetch_model_config method + def mock_fetch_model_config_func(_node_data_model): + return mock_model_instance, mock_model_config + + # Also mock ModelManager.get_model_instance to avoid database calls + def mock_get_model_instance(_self, **kwargs): + return mock_model_instance + + with ( + patch.object(node, "_fetch_model_config", mock_fetch_model_config_func), + patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance), + ): + # execute node + result = node._run() + assert isinstance(result, Generator) + + for item in result: + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.process_data is not None + assert item.run_result.outputs is not None + assert item.run_result.outputs.get("text") is not None + assert item.run_result.outputs.get("usage", {})["total_tokens"] > 0 @pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) -def test_execute_llm_with_jinja2(app, setup_code_executor_mock): +def test_execute_llm_with_jinja2(flask_req_ctx, setup_code_executor_mock): """ Test execute LLM node with jinja2 """ - with app.app_context(): - node = init_llm_node( - config={ - "id": "llm", - "data": { - "title": "123", - "type": "llm", - "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, - "prompt_config": { - "jinja2_variables": [ - {"variable": "sys_query", "value_selector": ["sys", "query"]}, - {"variable": "output", "value_selector": ["abc", "output"]}, - ] - }, - "prompt_template": [ - { - "role": "system", - "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}", - "jinja2_text": "you are a helpful assistant.\ntoday's weather is {{output}}.", - "edition_type": "jinja2", - }, - { - "role": "user", - "text": "{{#sys.query#}}", - "jinja2_text": "{{sys_query}}", - "edition_type": "basic", - }, - ], - "memory": None, - "context": {"enabled": False}, - "vision": {"enabled": False}, - }, - }, - ) - - # Mock db.session.close() - db.session.close = MagicMock() - - # Create a proper LLM result with real entities - mock_usage = LLMUsage( - prompt_tokens=30, - prompt_unit_price=Decimal("0.001"), - prompt_price_unit=Decimal("1000"), - prompt_price=Decimal("0.00003"), - completion_tokens=20, - completion_unit_price=Decimal("0.002"), - completion_price_unit=Decimal("1000"), - completion_price=Decimal("0.00004"), - total_tokens=50, - total_price=Decimal("0.00007"), - currency="USD", - latency=0.5, - ) - - mock_message = AssistantPromptMessage(content="Test response: sunny weather and what's the weather today?") - - mock_llm_result = LLMResult( - model="gpt-3.5-turbo", - prompt_messages=[], - message=mock_message, - usage=mock_usage, - ) - - # Create a simple mock model instance that doesn't call real providers - mock_model_instance = MagicMock() - mock_model_instance.invoke_llm.return_value = mock_llm_result - - # Create a simple mock model config with required attributes - mock_model_config = MagicMock() - mock_model_config.mode = "chat" - mock_model_config.provider = "openai" - mock_model_config.model = "gpt-3.5-turbo" - mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b" - - # Mock the _fetch_model_config method - def mock_fetch_model_config_func(_node_data_model): - return mock_model_instance, mock_model_config - - # Also mock ModelManager.get_model_instance to avoid database calls - def mock_get_model_instance(_self, **kwargs): - return mock_model_instance - - with ( - patch.object(node, "_fetch_model_config", mock_fetch_model_config_func), - patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance), - ): - # execute node - result = node._run() - - for item in result: - if isinstance(item, RunCompletedEvent): - assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.process_data is not None - assert "sunny" in json.dumps(item.run_result.process_data) - assert "what's the weather today?" in json.dumps(item.run_result.process_data) - - -def test_extract_json(): node = init_llm_node( config={ "id": "llm", @@ -304,21 +188,95 @@ def test_extract_json(): "type": "llm", "model": {"provider": "openai", "name": "gpt-3.5-turbo", "mode": "chat", "completion_params": {}}, "prompt_config": { - "structured_output": { - "enabled": True, - "schema": { - "type": "object", - "properties": {"name": {"type": "string"}, "age": {"type": "number"}}, - }, - } + "jinja2_variables": [ + {"variable": "sys_query", "value_selector": ["sys", "query"]}, + {"variable": "output", "value_selector": ["abc", "output"]}, + ] }, - "prompt_template": [{"role": "user", "text": "{{#sys.query#}}"}], + "prompt_template": [ + { + "role": "system", + "text": "you are a helpful assistant.\ntoday's weather is {{#abc.output#}}", + "jinja2_text": "you are a helpful assistant.\ntoday's weather is {{output}}.", + "edition_type": "jinja2", + }, + { + "role": "user", + "text": "{{#sys.query#}}", + "jinja2_text": "{{sys_query}}", + "edition_type": "basic", + }, + ], "memory": None, "context": {"enabled": False}, "vision": {"enabled": False}, }, }, ) + + # Mock db.session.close() + db.session.close = MagicMock() + + # Create a proper LLM result with real entities + mock_usage = LLMUsage( + prompt_tokens=30, + prompt_unit_price=Decimal("0.001"), + prompt_price_unit=Decimal(1000), + prompt_price=Decimal("0.00003"), + completion_tokens=20, + completion_unit_price=Decimal("0.002"), + completion_price_unit=Decimal(1000), + completion_price=Decimal("0.00004"), + total_tokens=50, + total_price=Decimal("0.00007"), + currency="USD", + latency=0.5, + ) + + mock_message = AssistantPromptMessage(content="Test response: sunny weather and what's the weather today?") + + mock_llm_result = LLMResult( + model="gpt-3.5-turbo", + prompt_messages=[], + message=mock_message, + usage=mock_usage, + ) + + # Create a simple mock model instance that doesn't call real providers + mock_model_instance = MagicMock() + mock_model_instance.invoke_llm.return_value = mock_llm_result + + # Create a simple mock model config with required attributes + mock_model_config = MagicMock() + mock_model_config.mode = "chat" + mock_model_config.provider = "openai" + mock_model_config.model = "gpt-3.5-turbo" + mock_model_config.provider_model_bundle.configuration.tenant_id = "9d2074fc-6f86-45a9-b09d-6ecc63b9056b" + + # Mock the _fetch_model_config method + def mock_fetch_model_config_func(_node_data_model): + return mock_model_instance, mock_model_config + + # Also mock ModelManager.get_model_instance to avoid database calls + def mock_get_model_instance(_self, **kwargs): + return mock_model_instance + + with ( + patch.object(node, "_fetch_model_config", mock_fetch_model_config_func), + patch("core.model_manager.ModelManager.get_model_instance", mock_get_model_instance), + ): + # execute node + result = node._run() + + for item in result: + if isinstance(item, RunCompletedEvent): + assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert item.run_result.process_data is not None + assert "sunny" in json.dumps(item.run_result.process_data) + assert "what's the weather today?" in json.dumps(item.run_result.process_data) + + +def test_extract_json(): llm_texts = [ '\n\n{"name": "test", "age": 123', # resoning model (deepseek-r1) '{"name":"test","age":123}', # json schema model (gpt-4o) @@ -327,4 +285,4 @@ def test_extract_json(): '{"name":"test",age:123}', # without quotes (qwen-2.5-0.5b) ] result = {"name": "test", "age": 123} - assert all(node._parse_structured_output(item) == result for item in llm_texts) + assert all(_parse_structured_output(item) == result for item in llm_texts) diff --git a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py b/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py index e89e03ae86..dd8466afa6 100644 --- a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py +++ b/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py @@ -8,11 +8,11 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.model_runtime.entities import AssistantPromptMessage from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode +from core.workflow.system_variable import SystemVariable from extensions.ext_database import db from models.enums import UserFrom from tests.integration_tests.workflow.nodes.__mock.model import get_mocked_fetch_model_config @@ -64,12 +64,9 @@ def init_parameter_extractor_node(config: dict): # construct variable pool variable_pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "what's the weather in SF", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, + system_variables=SystemVariable( + user_id="aaa", files=[], query="what's the weather in SF", conversation_id="abababa" + ), user_inputs={}, environment_variables=[], conversation_variables=[], @@ -353,7 +350,7 @@ def test_extract_json_from_tool_call(): assert result["location"] == "kawaii" -def test_chat_parameter_extractor_with_memory(setup_model_mock): +def test_chat_parameter_extractor_with_memory(setup_model_mock, monkeypatch): """ Test chat parameter extractor with memory. """ @@ -384,7 +381,8 @@ def test_chat_parameter_extractor_with_memory(setup_model_mock): mode="chat", credentials={"openai_api_key": os.environ.get("OPENAI_API_KEY")}, ) - node._fetch_memory = get_mocked_fetch_memory("customized memory") + # Test the mock before running the actual test + monkeypatch.setattr("core.workflow.nodes.llm.llm_utils.fetch_memory", get_mocked_fetch_memory("customized memory")) db.session.close = MagicMock() result = node._run() diff --git a/api/tests/integration_tests/workflow/nodes/test_template_transform.py b/api/tests/integration_tests/workflow/nodes/test_template_transform.py index a5f2677a59..1f617fc92d 100644 --- a/api/tests/integration_tests/workflow/nodes/test_template_transform.py +++ b/api/tests/integration_tests/workflow/nodes/test_template_transform.py @@ -6,11 +6,11 @@ import pytest from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType from tests.integration_tests.workflow.nodes.__mock.code_executor import setup_code_executor_mock @@ -61,7 +61,7 @@ def test_execute_code(setup_code_executor_mock): # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], conversation_variables=[], diff --git a/api/tests/integration_tests/workflow/nodes/test_tool.py b/api/tests/integration_tests/workflow/nodes/test_tool.py index 039beedafe..6907e0163e 100644 --- a/api/tests/integration_tests/workflow/nodes/test_tool.py +++ b/api/tests/integration_tests/workflow/nodes/test_tool.py @@ -6,12 +6,12 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.utils.configuration import ToolParameterConfigurationManager from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.event.event import RunCompletedEvent from core.workflow.nodes.tool.tool_node import ToolNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -44,7 +44,7 @@ def init_tool_node(config: dict): # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], conversation_variables=[], diff --git a/api/tests/unit_tests/configs/test_dify_config.py b/api/tests/unit_tests/configs/test_dify_config.py index cac0a688cd..e9d4ee1935 100644 --- a/api/tests/unit_tests/configs/test_dify_config.py +++ b/api/tests/unit_tests/configs/test_dify_config.py @@ -1,6 +1,7 @@ import os from flask import Flask +from packaging.version import Version from yarl import URL from configs.app_config import DifyConfig @@ -40,6 +41,9 @@ def test_dify_config(monkeypatch): assert config.WORKFLOW_PARALLEL_DEPTH_LIMIT == 3 + # values from pyproject.toml + assert Version(config.project.version) >= Version("1.0.0") + # NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected. # This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`. @@ -84,6 +88,7 @@ def test_flask_configs(monkeypatch): "pool_pre_ping": False, "pool_recycle": 3600, "pool_size": 30, + "pool_use_lifo": False, } assert config["CONSOLE_WEB_URL"] == "https://example.com" diff --git a/api/tests/unit_tests/conftest.py b/api/tests/unit_tests/conftest.py index e09acc4c39..077ffe3408 100644 --- a/api/tests/unit_tests/conftest.py +++ b/api/tests/unit_tests/conftest.py @@ -1,4 +1,5 @@ import os +from unittest.mock import MagicMock, patch import pytest from flask import Flask @@ -11,6 +12,24 @@ PROJECT_DIR = os.path.abspath(os.path.join(ABS_PATH, os.pardir, os.pardir)) CACHED_APP = Flask(__name__) +# set global mock for Redis client +redis_mock = MagicMock() +redis_mock.get = MagicMock(return_value=None) +redis_mock.setex = MagicMock() +redis_mock.setnx = MagicMock() +redis_mock.delete = MagicMock() +redis_mock.lock = MagicMock() +redis_mock.exists = MagicMock(return_value=False) +redis_mock.set = MagicMock() +redis_mock.expire = MagicMock() +redis_mock.hgetall = MagicMock(return_value={}) +redis_mock.hdel = MagicMock() +redis_mock.incr = MagicMock(return_value=1) + +# apply the mock to the Redis client in the Flask app +redis_patcher = patch("extensions.ext_redis.redis_client", redis_mock) +redis_patcher.start() + @pytest.fixture def app() -> Flask: @@ -21,3 +40,19 @@ def app() -> Flask: def _provide_app_context(app: Flask): with app.app_context(): yield + + +@pytest.fixture(autouse=True) +def reset_redis_mock(): + """reset the Redis mock before each test""" + redis_mock.reset_mock() + redis_mock.get.return_value = None + redis_mock.setex.return_value = None + redis_mock.setnx.return_value = None + redis_mock.delete.return_value = None + redis_mock.exists.return_value = False + redis_mock.set.return_value = None + redis_mock.expire.return_value = None + redis_mock.hgetall.return_value = {} + redis_mock.hdel.return_value = None + redis_mock.incr.return_value = 1 diff --git a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py new file mode 100644 index 0000000000..f26be6702a --- /dev/null +++ b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py @@ -0,0 +1,302 @@ +import datetime +import uuid +from collections import OrderedDict +from typing import Any, NamedTuple + +from flask_restful import marshal + +from controllers.console.app.workflow_draft_variable import ( + _WORKFLOW_DRAFT_VARIABLE_FIELDS, + _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS, + _WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS, + _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS, +) +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from factories.variable_factory import build_segment +from models.workflow import WorkflowDraftVariable +from services.workflow_draft_variable_service import WorkflowDraftVariableList + +_TEST_APP_ID = "test_app_id" +_TEST_NODE_EXEC_ID = str(uuid.uuid4()) + + +class TestWorkflowDraftVariableFields: + def test_conversation_variable(self): + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=_TEST_APP_ID, name="conv_var", value=build_segment(1) + ) + + conv_var.id = str(uuid.uuid4()) + conv_var.visible = True + + expected_without_value: OrderedDict[str, Any] = OrderedDict( + { + "id": str(conv_var.id), + "type": conv_var.get_variable_type().value, + "name": "conv_var", + "description": "", + "selector": [CONVERSATION_VARIABLE_NODE_ID, "conv_var"], + "value_type": "number", + "edited": False, + "visible": True, + } + ) + + assert marshal(conv_var, _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS) == expected_without_value + expected_with_value = expected_without_value.copy() + expected_with_value["value"] = 1 + assert marshal(conv_var, _WORKFLOW_DRAFT_VARIABLE_FIELDS) == expected_with_value + + def test_create_sys_variable(self): + sys_var = WorkflowDraftVariable.new_sys_variable( + app_id=_TEST_APP_ID, + name="sys_var", + value=build_segment("a"), + editable=True, + node_execution_id=_TEST_NODE_EXEC_ID, + ) + + sys_var.id = str(uuid.uuid4()) + sys_var.last_edited_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + sys_var.visible = True + + expected_without_value = OrderedDict( + { + "id": str(sys_var.id), + "type": sys_var.get_variable_type().value, + "name": "sys_var", + "description": "", + "selector": [SYSTEM_VARIABLE_NODE_ID, "sys_var"], + "value_type": "string", + "edited": True, + "visible": True, + } + ) + assert marshal(sys_var, _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS) == expected_without_value + expected_with_value = expected_without_value.copy() + expected_with_value["value"] = "a" + assert marshal(sys_var, _WORKFLOW_DRAFT_VARIABLE_FIELDS) == expected_with_value + + def test_node_variable(self): + node_var = WorkflowDraftVariable.new_node_variable( + app_id=_TEST_APP_ID, + node_id="test_node", + name="node_var", + value=build_segment([1, "a"]), + visible=False, + node_execution_id=_TEST_NODE_EXEC_ID, + ) + + node_var.id = str(uuid.uuid4()) + node_var.last_edited_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + + expected_without_value: OrderedDict[str, Any] = OrderedDict( + { + "id": str(node_var.id), + "type": node_var.get_variable_type().value, + "name": "node_var", + "description": "", + "selector": ["test_node", "node_var"], + "value_type": "array[any]", + "edited": True, + "visible": False, + } + ) + + assert marshal(node_var, _WORKFLOW_DRAFT_VARIABLE_WITHOUT_VALUE_FIELDS) == expected_without_value + expected_with_value = expected_without_value.copy() + expected_with_value["value"] = [1, "a"] + assert marshal(node_var, _WORKFLOW_DRAFT_VARIABLE_FIELDS) == expected_with_value + + +class TestWorkflowDraftVariableList: + def test_workflow_draft_variable_list(self): + class TestCase(NamedTuple): + name: str + var_list: WorkflowDraftVariableList + expected: dict + + node_var = WorkflowDraftVariable.new_node_variable( + app_id=_TEST_APP_ID, + node_id="test_node", + name="test_var", + value=build_segment("a"), + visible=True, + node_execution_id=_TEST_NODE_EXEC_ID, + ) + node_var.id = str(uuid.uuid4()) + node_var_dict = OrderedDict( + { + "id": str(node_var.id), + "type": node_var.get_variable_type().value, + "name": "test_var", + "description": "", + "selector": ["test_node", "test_var"], + "value_type": "string", + "edited": False, + "visible": True, + } + ) + + cases = [ + TestCase( + name="empty variable list", + var_list=WorkflowDraftVariableList(variables=[]), + expected=OrderedDict( + { + "items": [], + "total": None, + } + ), + ), + TestCase( + name="empty variable list with total", + var_list=WorkflowDraftVariableList(variables=[], total=10), + expected=OrderedDict( + { + "items": [], + "total": 10, + } + ), + ), + TestCase( + name="non-empty variable list", + var_list=WorkflowDraftVariableList(variables=[node_var], total=None), + expected=OrderedDict( + { + "items": [node_var_dict], + "total": None, + } + ), + ), + TestCase( + name="non-empty variable list with total", + var_list=WorkflowDraftVariableList(variables=[node_var], total=10), + expected=OrderedDict( + { + "items": [node_var_dict], + "total": 10, + } + ), + ), + ] + + for idx, case in enumerate(cases, 1): + assert marshal(case.var_list, _WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS) == case.expected, ( + f"Test case {idx} failed, {case.name=}" + ) + + +def test_workflow_node_variables_fields(): + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=_TEST_APP_ID, name="conv_var", value=build_segment(1) + ) + resp = marshal(WorkflowDraftVariableList(variables=[conv_var]), _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + assert isinstance(resp, dict) + assert len(resp["items"]) == 1 + item_dict = resp["items"][0] + assert item_dict["name"] == "conv_var" + assert item_dict["value"] == 1 + + +def test_workflow_file_variable_with_signed_url(): + """Test that File type variables include signed URLs in API responses.""" + from core.file.enums import FileTransferMethod, FileType + from core.file.models import File + + # Create a File object with LOCAL_FILE transfer method (which generates signed URLs) + test_file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="test_upload_file_id", + filename="test.jpg", + extension=".jpg", + mime_type="image/jpeg", + size=12345, + ) + + # Create a WorkflowDraftVariable with the File + file_var = WorkflowDraftVariable.new_node_variable( + app_id=_TEST_APP_ID, + node_id="test_node", + name="file_var", + value=build_segment(test_file), + node_execution_id=_TEST_NODE_EXEC_ID, + ) + + # Marshal the variable using the API fields + resp = marshal(WorkflowDraftVariableList(variables=[file_var]), _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + + # Verify the response structure + assert isinstance(resp, dict) + assert len(resp["items"]) == 1 + item_dict = resp["items"][0] + assert item_dict["name"] == "file_var" + + # Verify the value is a dict (File.to_dict() result) and contains expected fields + value = item_dict["value"] + assert isinstance(value, dict) + + # Verify the File fields are preserved + assert value["id"] == test_file.id + assert value["filename"] == test_file.filename + assert value["type"] == test_file.type.value + assert value["transfer_method"] == test_file.transfer_method.value + assert value["size"] == test_file.size + + # Verify the URL is present (it should be a signed URL for LOCAL_FILE transfer method) + remote_url = value["remote_url"] + assert remote_url is not None + + assert isinstance(remote_url, str) + # For LOCAL_FILE, the URL should contain signature parameters + assert "timestamp=" in remote_url + assert "nonce=" in remote_url + assert "sign=" in remote_url + + +def test_workflow_file_variable_remote_url(): + """Test that File type variables with REMOTE_URL transfer method return the remote URL.""" + from core.file.enums import FileTransferMethod, FileType + from core.file.models import File + + # Create a File object with REMOTE_URL transfer method + test_file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://example.com/test.jpg", + filename="test.jpg", + extension=".jpg", + mime_type="image/jpeg", + size=12345, + ) + + # Create a WorkflowDraftVariable with the File + file_var = WorkflowDraftVariable.new_node_variable( + app_id=_TEST_APP_ID, + node_id="test_node", + name="file_var", + value=build_segment(test_file), + node_execution_id=_TEST_NODE_EXEC_ID, + ) + + # Marshal the variable using the API fields + resp = marshal(WorkflowDraftVariableList(variables=[file_var]), _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS) + + # Verify the response structure + assert isinstance(resp, dict) + assert len(resp["items"]) == 1 + item_dict = resp["items"][0] + assert item_dict["name"] == "file_var" + + # Verify the value is a dict (File.to_dict() result) and contains expected fields + value = item_dict["value"] + assert isinstance(value, dict) + remote_url = value["remote_url"] + + # For REMOTE_URL, the URL should be the original remote URL + assert remote_url == test_file.remote_url diff --git a/api/tests/unit_tests/controllers/console/test_wraps.py b/api/tests/unit_tests/controllers/console/test_wraps.py new file mode 100644 index 0000000000..9742368f04 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/test_wraps.py @@ -0,0 +1,380 @@ +from unittest.mock import MagicMock, patch + +import pytest +from flask import Flask +from flask_login import LoginManager, UserMixin + +from controllers.console.error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout +from controllers.console.workspace.error import AccountNotInitializedError +from controllers.console.wraps import ( + account_initialization_required, + cloud_edition_billing_rate_limit_check, + cloud_edition_billing_resource_check, + enterprise_license_required, + only_edition_cloud, + only_edition_enterprise, + only_edition_self_hosted, + setup_required, +) +from models.account import AccountStatus +from services.feature_service import LicenseStatus + + +class MockUser(UserMixin): + """Simple User class for testing.""" + + def __init__(self, user_id: str): + self.id = user_id + self.current_tenant_id = "tenant123" + + def get_id(self) -> str: + return self.id + + +def create_app_with_login(): + """Create a Flask app with LoginManager configured.""" + app = Flask(__name__) + app.config["SECRET_KEY"] = "test-secret-key" + + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(user_id: str): + return MockUser(user_id) + + return app + + +class TestAccountInitialization: + """Test account initialization decorator""" + + def test_should_allow_initialized_account(self): + """Test that initialized accounts can access protected views""" + # Arrange + mock_user = MagicMock() + mock_user.status = AccountStatus.ACTIVE + + @account_initialization_required + def protected_view(): + return "success" + + # Act + with patch("controllers.console.wraps.current_user", mock_user): + result = protected_view() + + # Assert + assert result == "success" + + def test_should_reject_uninitialized_account(self): + """Test that uninitialized accounts raise AccountNotInitializedError""" + # Arrange + mock_user = MagicMock() + mock_user.status = AccountStatus.UNINITIALIZED + + @account_initialization_required + def protected_view(): + return "success" + + # Act & Assert + with patch("controllers.console.wraps.current_user", mock_user): + with pytest.raises(AccountNotInitializedError): + protected_view() + + +class TestEditionChecks: + """Test edition-specific decorators""" + + def test_only_edition_cloud_allows_cloud_edition(self): + """Test cloud edition decorator allows CLOUD edition""" + + # Arrange + @only_edition_cloud + def cloud_view(): + return "cloud_success" + + # Act + with patch("controllers.console.wraps.dify_config.EDITION", "CLOUD"): + result = cloud_view() + + # Assert + assert result == "cloud_success" + + def test_only_edition_cloud_rejects_other_editions(self): + """Test cloud edition decorator rejects non-CLOUD editions""" + # Arrange + app = Flask(__name__) + + @only_edition_cloud + def cloud_view(): + return "cloud_success" + + # Act & Assert + with app.test_request_context(): + with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"): + with pytest.raises(Exception) as exc_info: + cloud_view() + assert exc_info.value.code == 404 + + def test_only_edition_enterprise_allows_when_enabled(self): + """Test enterprise edition decorator allows when ENTERPRISE_ENABLED is True""" + + # Arrange + @only_edition_enterprise + def enterprise_view(): + return "enterprise_success" + + # Act + with patch("controllers.console.wraps.dify_config.ENTERPRISE_ENABLED", True): + result = enterprise_view() + + # Assert + assert result == "enterprise_success" + + def test_only_edition_self_hosted_allows_self_hosted(self): + """Test self-hosted edition decorator allows SELF_HOSTED edition""" + + # Arrange + @only_edition_self_hosted + def self_hosted_view(): + return "self_hosted_success" + + # Act + with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"): + result = self_hosted_view() + + # Assert + assert result == "self_hosted_success" + + +class TestBillingResourceLimits: + """Test billing resource limit decorators""" + + def test_should_allow_when_under_resource_limit(self): + """Test that requests are allowed when under resource limits""" + # Arrange + mock_features = MagicMock() + mock_features.billing.enabled = True + mock_features.members.limit = 10 + mock_features.members.size = 5 + + @cloud_edition_billing_resource_check("members") + def add_member(): + return "member_added" + + # Act + with patch("controllers.console.wraps.current_user"): + with patch("controllers.console.wraps.FeatureService.get_features", return_value=mock_features): + result = add_member() + + # Assert + assert result == "member_added" + + def test_should_reject_when_over_resource_limit(self): + """Test that requests are rejected when over resource limits""" + # Arrange + app = create_app_with_login() + mock_features = MagicMock() + mock_features.billing.enabled = True + mock_features.members.limit = 10 + mock_features.members.size = 10 + + @cloud_edition_billing_resource_check("members") + def add_member(): + return "member_added" + + # Act & Assert + with app.test_request_context(): + with patch("controllers.console.wraps.current_user", MockUser("test_user")): + with patch("controllers.console.wraps.FeatureService.get_features", return_value=mock_features): + with pytest.raises(Exception) as exc_info: + add_member() + assert exc_info.value.code == 403 + assert "members has reached the limit" in str(exc_info.value.description) + + def test_should_check_source_for_documents_limit(self): + """Test document limit checks request source""" + # Arrange + app = create_app_with_login() + mock_features = MagicMock() + mock_features.billing.enabled = True + mock_features.documents_upload_quota.limit = 100 + mock_features.documents_upload_quota.size = 100 + + @cloud_edition_billing_resource_check("documents") + def upload_document(): + return "document_uploaded" + + # Test 1: Should reject when source is datasets + with app.test_request_context("/?source=datasets"): + with patch("controllers.console.wraps.current_user", MockUser("test_user")): + with patch("controllers.console.wraps.FeatureService.get_features", return_value=mock_features): + with pytest.raises(Exception) as exc_info: + upload_document() + assert exc_info.value.code == 403 + + # Test 2: Should allow when source is not datasets + with app.test_request_context("/?source=other"): + with patch("controllers.console.wraps.current_user", MockUser("test_user")): + with patch("controllers.console.wraps.FeatureService.get_features", return_value=mock_features): + result = upload_document() + assert result == "document_uploaded" + + +class TestRateLimiting: + """Test rate limiting decorator""" + + @patch("controllers.console.wraps.redis_client") + @patch("controllers.console.wraps.db") + def test_should_allow_requests_within_rate_limit(self, mock_db, mock_redis): + """Test that requests within rate limit are allowed""" + # Arrange + mock_rate_limit = MagicMock() + mock_rate_limit.enabled = True + mock_rate_limit.limit = 10 + mock_redis.zcard.return_value = 5 # 5 requests in window + + @cloud_edition_billing_rate_limit_check("knowledge") + def knowledge_request(): + return "knowledge_success" + + # Act + with patch("controllers.console.wraps.current_user"): + with patch( + "controllers.console.wraps.FeatureService.get_knowledge_rate_limit", return_value=mock_rate_limit + ): + result = knowledge_request() + + # Assert + assert result == "knowledge_success" + mock_redis.zadd.assert_called_once() + mock_redis.zremrangebyscore.assert_called_once() + + @patch("controllers.console.wraps.redis_client") + @patch("controllers.console.wraps.db") + def test_should_reject_requests_over_rate_limit(self, mock_db, mock_redis): + """Test that requests over rate limit are rejected and logged""" + # Arrange + app = create_app_with_login() + mock_rate_limit = MagicMock() + mock_rate_limit.enabled = True + mock_rate_limit.limit = 10 + mock_rate_limit.subscription_plan = "pro" + mock_redis.zcard.return_value = 11 # Over limit + + mock_session = MagicMock() + mock_db.session = mock_session + + @cloud_edition_billing_rate_limit_check("knowledge") + def knowledge_request(): + return "knowledge_success" + + # Act & Assert + with app.test_request_context(): + with patch("controllers.console.wraps.current_user", MockUser("test_user")): + with patch( + "controllers.console.wraps.FeatureService.get_knowledge_rate_limit", return_value=mock_rate_limit + ): + with pytest.raises(Exception) as exc_info: + knowledge_request() + + # Verify error + assert exc_info.value.code == 403 + assert "rate limit" in str(exc_info.value.description) + + # Verify rate limit log was created + mock_session.add.assert_called_once() + mock_session.commit.assert_called_once() + + +class TestSystemSetup: + """Test system setup decorator""" + + @patch("controllers.console.wraps.db") + def test_should_allow_when_setup_complete(self, mock_db): + """Test that requests are allowed when setup is complete""" + # Arrange + mock_db.session.query.return_value.first.return_value = MagicMock() # Setup exists + + @setup_required + def admin_view(): + return "admin_success" + + # Act + with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"): + result = admin_view() + + # Assert + assert result == "admin_success" + + @patch("controllers.console.wraps.db") + @patch("controllers.console.wraps.os.environ.get") + def test_should_raise_not_init_validate_error_with_init_password(self, mock_environ_get, mock_db): + """Test NotInitValidateError when INIT_PASSWORD is set but setup not complete""" + # Arrange + mock_db.session.query.return_value.first.return_value = None # No setup + mock_environ_get.return_value = "some_password" + + @setup_required + def admin_view(): + return "admin_success" + + # Act & Assert + with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"): + with pytest.raises(NotInitValidateError): + admin_view() + + @patch("controllers.console.wraps.db") + @patch("controllers.console.wraps.os.environ.get") + def test_should_raise_not_setup_error_without_init_password(self, mock_environ_get, mock_db): + """Test NotSetupError when no INIT_PASSWORD and setup not complete""" + # Arrange + mock_db.session.query.return_value.first.return_value = None # No setup + mock_environ_get.return_value = None # No INIT_PASSWORD + + @setup_required + def admin_view(): + return "admin_success" + + # Act & Assert + with patch("controllers.console.wraps.dify_config.EDITION", "SELF_HOSTED"): + with pytest.raises(NotSetupError): + admin_view() + + +class TestEnterpriseLicense: + """Test enterprise license decorator""" + + def test_should_allow_with_valid_license(self): + """Test that valid licenses allow access""" + # Arrange + mock_settings = MagicMock() + mock_settings.license.status = LicenseStatus.ACTIVE + + @enterprise_license_required + def enterprise_feature(): + return "enterprise_success" + + # Act + with patch("controllers.console.wraps.FeatureService.get_system_features", return_value=mock_settings): + result = enterprise_feature() + + # Assert + assert result == "enterprise_success" + + @pytest.mark.parametrize("invalid_status", [LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST]) + def test_should_reject_with_invalid_license(self, invalid_status): + """Test that invalid licenses raise UnauthorizedAndForceLogout""" + # Arrange + mock_settings = MagicMock() + mock_settings.license.status = invalid_status + + @enterprise_license_required + def enterprise_feature(): + return "enterprise_success" + + # Act & Assert + with patch("controllers.console.wraps.FeatureService.get_system_features", return_value=mock_settings): + with pytest.raises(UnauthorizedAndForceLogout) as exc_info: + enterprise_feature() + assert "license is invalid" in str(exc_info.value) diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py new file mode 100644 index 0000000000..b88a57bfd4 --- /dev/null +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py @@ -0,0 +1,259 @@ +from collections.abc import Mapping, Sequence + +from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter +from core.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType +from core.variables.segments import ArrayFileSegment, FileSegment + + +class TestWorkflowResponseConverterFetchFilesFromVariableValue: + """Test class for WorkflowResponseConverter._fetch_files_from_variable_value method""" + + def create_test_file(self, file_id: str = "test_file_1") -> File: + """Create a test File object""" + return File( + id=file_id, + tenant_id="test_tenant", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="related_123", + filename=f"{file_id}.txt", + extension=".txt", + mime_type="text/plain", + size=1024, + storage_key="storage_key_123", + ) + + def create_file_dict(self, file_id: str = "test_file_dict") -> dict: + """Create a file dictionary with correct dify_model_identity""" + return { + "dify_model_identity": FILE_MODEL_IDENTITY, + "id": file_id, + "tenant_id": "test_tenant", + "type": "document", + "transfer_method": "local_file", + "related_id": "related_456", + "filename": f"{file_id}.txt", + "extension": ".txt", + "mime_type": "text/plain", + "size": 2048, + "url": "http://example.com/file.txt", + } + + def test_fetch_files_from_variable_value_with_none(self): + """Test with None input""" + # The method signature expects Union[dict, list, Segment], but implementation handles None + # We'll test the actual behavior by passing an empty dict instead + result = WorkflowResponseConverter._fetch_files_from_variable_value(None) # type: ignore + assert result == [] + + def test_fetch_files_from_variable_value_with_empty_dict(self): + """Test with empty dictionary""" + result = WorkflowResponseConverter._fetch_files_from_variable_value({}) + assert result == [] + + def test_fetch_files_from_variable_value_with_empty_list(self): + """Test with empty list""" + result = WorkflowResponseConverter._fetch_files_from_variable_value([]) + assert result == [] + + def test_fetch_files_from_variable_value_with_file_segment(self): + """Test with valid FileSegment""" + test_file = self.create_test_file("segment_file") + file_segment = FileSegment(value=test_file) + + result = WorkflowResponseConverter._fetch_files_from_variable_value(file_segment) + + assert len(result) == 1 + assert isinstance(result[0], dict) + assert result[0]["id"] == "segment_file" + assert result[0]["dify_model_identity"] == FILE_MODEL_IDENTITY + + def test_fetch_files_from_variable_value_with_array_file_segment_single(self): + """Test with ArrayFileSegment containing single file""" + test_file = self.create_test_file("array_file_1") + array_segment = ArrayFileSegment(value=[test_file]) + + result = WorkflowResponseConverter._fetch_files_from_variable_value(array_segment) + + assert len(result) == 1 + assert isinstance(result[0], dict) + assert result[0]["id"] == "array_file_1" + + def test_fetch_files_from_variable_value_with_array_file_segment_multiple(self): + """Test with ArrayFileSegment containing multiple files""" + test_file_1 = self.create_test_file("array_file_1") + test_file_2 = self.create_test_file("array_file_2") + array_segment = ArrayFileSegment(value=[test_file_1, test_file_2]) + + result = WorkflowResponseConverter._fetch_files_from_variable_value(array_segment) + + assert len(result) == 2 + assert result[0]["id"] == "array_file_1" + assert result[1]["id"] == "array_file_2" + + def test_fetch_files_from_variable_value_with_array_file_segment_empty(self): + """Test with ArrayFileSegment containing empty array""" + array_segment = ArrayFileSegment(value=[]) + + result = WorkflowResponseConverter._fetch_files_from_variable_value(array_segment) + + assert result == [] + + def test_fetch_files_from_variable_value_with_list_of_file_dicts(self): + """Test with list containing file dictionaries""" + file_dict_1 = self.create_file_dict("list_file_1") + file_dict_2 = self.create_file_dict("list_file_2") + test_list = [file_dict_1, file_dict_2] + + result = WorkflowResponseConverter._fetch_files_from_variable_value(test_list) + + assert len(result) == 2 + assert result[0]["id"] == "list_file_1" + assert result[1]["id"] == "list_file_2" + + def test_fetch_files_from_variable_value_with_list_of_file_objects(self): + """Test with list containing File objects""" + file_obj_1 = self.create_test_file("list_obj_1") + file_obj_2 = self.create_test_file("list_obj_2") + test_list = [file_obj_1, file_obj_2] + + result = WorkflowResponseConverter._fetch_files_from_variable_value(test_list) + + assert len(result) == 2 + assert result[0]["id"] == "list_obj_1" + assert result[1]["id"] == "list_obj_2" + + def test_fetch_files_from_variable_value_with_list_mixed_valid_invalid(self): + """Test with list containing mix of valid files and invalid items""" + file_dict = self.create_file_dict("mixed_file") + invalid_dict = {"not_a_file": "value"} + test_list = [file_dict, invalid_dict, "string_item", 123] + + result = WorkflowResponseConverter._fetch_files_from_variable_value(test_list) + + assert len(result) == 1 + assert result[0]["id"] == "mixed_file" + + def test_fetch_files_from_variable_value_with_list_nested_structures(self): + """Test with list containing nested structures""" + file_dict = self.create_file_dict("nested_file") + nested_list = [file_dict, ["inner_list"]] + test_list = [nested_list, {"nested": "dict"}] + + result = WorkflowResponseConverter._fetch_files_from_variable_value(test_list) + + # Should not process nested structures in list items + assert result == [] + + def test_fetch_files_from_variable_value_with_dict_incorrect_identity(self): + """Test with dictionary having incorrect dify_model_identity""" + invalid_dict = {"dify_model_identity": "wrong_identity", "id": "invalid_file", "filename": "test.txt"} + + result = WorkflowResponseConverter._fetch_files_from_variable_value(invalid_dict) + + assert result == [] + + def test_fetch_files_from_variable_value_with_dict_missing_identity(self): + """Test with dictionary missing dify_model_identity""" + invalid_dict = {"id": "no_identity_file", "filename": "test.txt"} + + result = WorkflowResponseConverter._fetch_files_from_variable_value(invalid_dict) + + assert result == [] + + def test_fetch_files_from_variable_value_with_dict_file_object(self): + """Test with dictionary containing File object""" + file_obj = self.create_test_file("dict_obj_file") + test_dict = {"file_key": file_obj} + + result = WorkflowResponseConverter._fetch_files_from_variable_value(test_dict) + + # Should not extract File objects from dict values + assert result == [] + + def test_fetch_files_from_variable_value_with_mixed_data_types(self): + """Test with various mixed data types""" + mixed_data = {"string": "text", "number": 42, "boolean": True, "null": None, "dify_model_identity": "wrong"} + + result = WorkflowResponseConverter._fetch_files_from_variable_value(mixed_data) + + assert result == [] + + def test_fetch_files_from_variable_value_with_invalid_objects(self): + """Test with invalid objects that are not supported types""" + # Test with an invalid dict that doesn't match expected patterns + invalid_dict = {"custom_key": "custom_value"} + + result = WorkflowResponseConverter._fetch_files_from_variable_value(invalid_dict) + + assert result == [] + + def test_fetch_files_from_variable_value_with_string_input(self): + """Test with string input (unsupported type)""" + # Since method expects Union[dict, list, Segment], test with empty list instead + result = WorkflowResponseConverter._fetch_files_from_variable_value([]) + + assert result == [] + + def test_fetch_files_from_variable_value_with_number_input(self): + """Test with number input (unsupported type)""" + # Test with list containing numbers (should be ignored) + result = WorkflowResponseConverter._fetch_files_from_variable_value([42, "string", None]) + + assert result == [] + + def test_fetch_files_from_variable_value_return_type_is_sequence(self): + """Test that return type is Sequence[Mapping[str, Any]]""" + file_dict = self.create_file_dict("type_test_file") + + result = WorkflowResponseConverter._fetch_files_from_variable_value(file_dict) + + assert isinstance(result, Sequence) + assert len(result) == 1 + assert isinstance(result[0], Mapping) + assert all(isinstance(key, str) for key in result[0]) + + def test_fetch_files_from_variable_value_preserves_file_properties(self): + """Test that all file properties are preserved in the result""" + original_file = self.create_test_file("property_test") + file_segment = FileSegment(value=original_file) + + result = WorkflowResponseConverter._fetch_files_from_variable_value(file_segment) + + assert len(result) == 1 + file_dict = result[0] + assert file_dict["id"] == "property_test" + assert file_dict["tenant_id"] == "test_tenant" + assert file_dict["type"] == "document" + assert file_dict["transfer_method"] == "local_file" + assert file_dict["filename"] == "property_test.txt" + assert file_dict["extension"] == ".txt" + assert file_dict["mime_type"] == "text/plain" + assert file_dict["size"] == 1024 + + def test_fetch_files_from_variable_value_with_complex_nested_scenario(self): + """Test complex scenario with nested valid and invalid data""" + file_dict = self.create_file_dict("complex_file") + file_obj = self.create_test_file("complex_obj") + + # Complex nested structure + complex_data = [ + file_dict, # Valid file dict + file_obj, # Valid file object + { # Invalid dict + "not_file": "data", + "nested": {"deep": "value"}, + }, + [ # Nested list (should be ignored) + self.create_file_dict("nested_file") + ], + "string", # Invalid string + None, # None value + 42, # Invalid number + ] + + result = WorkflowResponseConverter._fetch_files_from_variable_value(complex_data) + + assert len(result) == 2 + assert result[0]["id"] == "complex_file" + assert result[1]["id"] == "complex_obj" diff --git a/api/tests/unit_tests/core/app/segments/test_factory.py b/api/tests/unit_tests/core/app/segments/test_factory.py deleted file mode 100644 index e6e289c12a..0000000000 --- a/api/tests/unit_tests/core/app/segments/test_factory.py +++ /dev/null @@ -1,165 +0,0 @@ -from uuid import uuid4 - -import pytest - -from core.variables import ( - ArrayNumberVariable, - ArrayObjectVariable, - ArrayStringVariable, - FloatVariable, - IntegerVariable, - ObjectSegment, - SecretVariable, - StringVariable, -) -from core.variables.exc import VariableError -from core.variables.segments import ArrayAnySegment -from factories import variable_factory - - -def test_string_variable(): - test_data = {"value_type": "string", "name": "test_text", "value": "Hello, World!"} - result = variable_factory.build_conversation_variable_from_mapping(test_data) - assert isinstance(result, StringVariable) - - -def test_integer_variable(): - test_data = {"value_type": "number", "name": "test_int", "value": 42} - result = variable_factory.build_conversation_variable_from_mapping(test_data) - assert isinstance(result, IntegerVariable) - - -def test_float_variable(): - test_data = {"value_type": "number", "name": "test_float", "value": 3.14} - result = variable_factory.build_conversation_variable_from_mapping(test_data) - assert isinstance(result, FloatVariable) - - -def test_secret_variable(): - test_data = {"value_type": "secret", "name": "test_secret", "value": "secret_value"} - result = variable_factory.build_conversation_variable_from_mapping(test_data) - assert isinstance(result, SecretVariable) - - -def test_invalid_value_type(): - test_data = {"value_type": "unknown", "name": "test_invalid", "value": "value"} - with pytest.raises(VariableError): - variable_factory.build_conversation_variable_from_mapping(test_data) - - -def test_build_a_blank_string(): - result = variable_factory.build_conversation_variable_from_mapping( - { - "value_type": "string", - "name": "blank", - "value": "", - } - ) - assert isinstance(result, StringVariable) - assert result.value == "" - - -def test_build_a_object_variable_with_none_value(): - var = variable_factory.build_segment( - { - "key1": None, - } - ) - assert isinstance(var, ObjectSegment) - assert var.value["key1"] is None - - -def test_object_variable(): - mapping = { - "id": str(uuid4()), - "value_type": "object", - "name": "test_object", - "description": "Description of the variable.", - "value": { - "key1": "text", - "key2": 2, - }, - } - variable = variable_factory.build_conversation_variable_from_mapping(mapping) - assert isinstance(variable, ObjectSegment) - assert isinstance(variable.value["key1"], str) - assert isinstance(variable.value["key2"], int) - - -def test_array_string_variable(): - mapping = { - "id": str(uuid4()), - "value_type": "array[string]", - "name": "test_array", - "description": "Description of the variable.", - "value": [ - "text", - "text", - ], - } - variable = variable_factory.build_conversation_variable_from_mapping(mapping) - assert isinstance(variable, ArrayStringVariable) - assert isinstance(variable.value[0], str) - assert isinstance(variable.value[1], str) - - -def test_array_number_variable(): - mapping = { - "id": str(uuid4()), - "value_type": "array[number]", - "name": "test_array", - "description": "Description of the variable.", - "value": [ - 1, - 2.0, - ], - } - variable = variable_factory.build_conversation_variable_from_mapping(mapping) - assert isinstance(variable, ArrayNumberVariable) - assert isinstance(variable.value[0], int) - assert isinstance(variable.value[1], float) - - -def test_array_object_variable(): - mapping = { - "id": str(uuid4()), - "value_type": "array[object]", - "name": "test_array", - "description": "Description of the variable.", - "value": [ - { - "key1": "text", - "key2": 1, - }, - { - "key1": "text", - "key2": 1, - }, - ], - } - variable = variable_factory.build_conversation_variable_from_mapping(mapping) - assert isinstance(variable, ArrayObjectVariable) - assert isinstance(variable.value[0], dict) - assert isinstance(variable.value[1], dict) - assert isinstance(variable.value[0]["key1"], str) - assert isinstance(variable.value[0]["key2"], int) - assert isinstance(variable.value[1]["key1"], str) - assert isinstance(variable.value[1]["key2"], int) - - -def test_variable_cannot_large_than_200_kb(): - with pytest.raises(VariableError): - variable_factory.build_conversation_variable_from_mapping( - { - "id": str(uuid4()), - "value_type": "string", - "name": "test_text", - "value": "a" * 1024 * 201, - } - ) - - -def test_array_none_variable(): - var = variable_factory.build_segment([None, None, None, None]) - assert isinstance(var, ArrayAnySegment) - assert var.value == [None, None, None, None] diff --git a/api/tests/unit_tests/core/app/segments/test_segment.py b/api/tests/unit_tests/core/app/segments/test_segment.py deleted file mode 100644 index 1b035d01a7..0000000000 --- a/api/tests/unit_tests/core/app/segments/test_segment.py +++ /dev/null @@ -1,58 +0,0 @@ -from core.helper import encrypter -from core.variables import SecretVariable, StringVariable -from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey - - -def test_segment_group_to_text(): - variable_pool = VariablePool( - system_variables={ - SystemVariableKey("user_id"): "fake-user-id", - }, - user_inputs={}, - environment_variables=[ - SecretVariable(name="secret_key", value="fake-secret-key"), - ], - conversation_variables=[], - ) - variable_pool.add(("node_id", "custom_query"), "fake-user-query") - template = ( - "Hello, {{#sys.user_id#}}! Your query is {{#node_id.custom_query#}}. And your key is {{#env.secret_key#}}." - ) - segments_group = variable_pool.convert_template(template) - - assert segments_group.text == "Hello, fake-user-id! Your query is fake-user-query. And your key is fake-secret-key." - assert segments_group.log == ( - f"Hello, fake-user-id! Your query is fake-user-query." - f" And your key is {encrypter.obfuscated_token('fake-secret-key')}." - ) - - -def test_convert_constant_to_segment_group(): - variable_pool = VariablePool( - system_variables={}, - user_inputs={}, - environment_variables=[], - conversation_variables=[], - ) - template = "Hello, world!" - segments_group = variable_pool.convert_template(template) - assert segments_group.text == "Hello, world!" - assert segments_group.log == "Hello, world!" - - -def test_convert_variable_to_segment_group(): - variable_pool = VariablePool( - system_variables={ - SystemVariableKey("user_id"): "fake-user-id", - }, - user_inputs={}, - environment_variables=[], - conversation_variables=[], - ) - template = "{{#sys.user_id#}}" - segments_group = variable_pool.convert_template(template) - assert segments_group.text == "fake-user-id" - assert segments_group.log == "fake-user-id" - assert isinstance(segments_group.value[0], StringVariable) - assert segments_group.value[0].value == "fake-user-id" diff --git a/api/tests/unit_tests/core/file/test_models.py b/api/tests/unit_tests/core/file/test_models.py new file mode 100644 index 0000000000..3ada2087c6 --- /dev/null +++ b/api/tests/unit_tests/core/file/test_models.py @@ -0,0 +1,25 @@ +from core.file import File, FileTransferMethod, FileType + + +def test_file(): + file = File( + id="test-file", + tenant_id="test-tenant-id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.TOOL_FILE, + related_id="test-related-id", + filename="image.png", + extension=".png", + mime_type="image/png", + size=67, + storage_key="test-storage-key", + url="https://example.com/image.png", + ) + assert file.tenant_id == "test-tenant-id" + assert file.type == FileType.IMAGE + assert file.transfer_method == FileTransferMethod.TOOL_FILE + assert file.related_id == "test-related-id" + assert file.filename == "image.png" + assert file.extension == ".png" + assert file.mime_type == "image/png" + assert file.size == 67 diff --git a/api/tests/unit_tests/core/helper/test_encrypter.py b/api/tests/unit_tests/core/helper/test_encrypter.py new file mode 100644 index 0000000000..61cf8f255d --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_encrypter.py @@ -0,0 +1,280 @@ +import base64 +import binascii +from unittest.mock import MagicMock, patch + +import pytest + +from core.helper.encrypter import ( + batch_decrypt_token, + decrypt_token, + encrypt_token, + get_decrypt_decoding, + obfuscated_token, +) +from libs.rsa import PrivkeyNotFoundError + + +class TestObfuscatedToken: + @pytest.mark.parametrize( + ("token", "expected"), + [ + ("", ""), # Empty token + ("1234567", "*" * 20), # Short token (<8 chars) + ("12345678", "*" * 20), # Boundary case (8 chars) + ("123456789abcdef", "123456" + "*" * 12 + "ef"), # Long token + ("abc!@#$%^&*()def", "abc!@#" + "*" * 12 + "ef"), # Special chars + ], + ) + def test_obfuscation_logic(self, token, expected): + """Test core obfuscation logic for various token lengths""" + assert obfuscated_token(token) == expected + + def test_sensitive_data_protection(self): + """Ensure obfuscation never reveals full sensitive data""" + token = "api_key_secret_12345" + obfuscated = obfuscated_token(token) + assert token not in obfuscated + assert "*" * 12 in obfuscated + + +class TestEncryptToken: + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_successful_encryption(self, mock_encrypt, mock_query): + """Test successful token encryption""" + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "mock_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + mock_encrypt.return_value = b"encrypted_data" + + result = encrypt_token("tenant-123", "test_token") + + assert result == base64.b64encode(b"encrypted_data").decode() + mock_encrypt.assert_called_with("test_token", "mock_public_key") + + @patch("models.engine.db.session.query") + def test_tenant_not_found(self, mock_query): + """Test error when tenant doesn't exist""" + mock_query.return_value.filter.return_value.first.return_value = None + + with pytest.raises(ValueError) as exc_info: + encrypt_token("invalid-tenant", "test_token") + + assert "Tenant with id invalid-tenant not found" in str(exc_info.value) + + +class TestDecryptToken: + @patch("libs.rsa.decrypt") + def test_successful_decryption(self, mock_decrypt): + """Test successful token decryption""" + mock_decrypt.return_value = "decrypted_token" + encrypted_data = base64.b64encode(b"encrypted_data").decode() + + result = decrypt_token("tenant-123", encrypted_data) + + assert result == "decrypted_token" + mock_decrypt.assert_called_once_with(b"encrypted_data", "tenant-123") + + def test_invalid_base64(self): + """Test handling of invalid base64 input""" + with pytest.raises(binascii.Error): + decrypt_token("tenant-123", "invalid_base64!!!") + + +class TestBatchDecryptToken: + @patch("libs.rsa.get_decrypt_decoding") + @patch("libs.rsa.decrypt_token_with_decoding") + def test_batch_decryption(self, mock_decrypt_with_decoding, mock_get_decoding): + """Test batch decryption functionality""" + mock_rsa_key = MagicMock() + mock_cipher_rsa = MagicMock() + mock_get_decoding.return_value = (mock_rsa_key, mock_cipher_rsa) + + # Test multiple tokens + mock_decrypt_with_decoding.side_effect = ["token1", "token2", "token3"] + tokens = [ + base64.b64encode(b"encrypted1").decode(), + base64.b64encode(b"encrypted2").decode(), + base64.b64encode(b"encrypted3").decode(), + ] + result = batch_decrypt_token("tenant-123", tokens) + + assert result == ["token1", "token2", "token3"] + # Key should only be loaded once + mock_get_decoding.assert_called_once_with("tenant-123") + + +class TestGetDecryptDecoding: + @patch("extensions.ext_redis.redis_client.get") + @patch("extensions.ext_storage.storage.load") + def test_private_key_not_found(self, mock_storage_load, mock_redis_get): + """Test error when private key file doesn't exist""" + mock_redis_get.return_value = None + mock_storage_load.side_effect = FileNotFoundError() + + with pytest.raises(PrivkeyNotFoundError) as exc_info: + get_decrypt_decoding("tenant-123") + + assert "Private key not found, tenant_id: tenant-123" in str(exc_info.value) + + +class TestEncryptDecryptIntegration: + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + @patch("libs.rsa.decrypt") + def test_should_encrypt_and_decrypt_consistently(self, mock_decrypt, mock_encrypt, mock_query): + """Test that encryption and decryption are consistent""" + # Setup mock tenant + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "mock_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + + # Setup mock encryption/decryption + original_token = "test_token_123" + mock_encrypt.return_value = b"encrypted_data" + mock_decrypt.return_value = original_token + + # Test encryption + encrypted = encrypt_token("tenant-123", original_token) + + # Test decryption + decrypted = decrypt_token("tenant-123", encrypted) + + assert decrypted == original_token + + +class TestSecurity: + """Critical security tests for encryption system""" + + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_cross_tenant_isolation(self, mock_encrypt, mock_query): + """Ensure tokens encrypted for one tenant cannot be used by another""" + # Setup mock tenant + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "tenant1_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + mock_encrypt.return_value = b"encrypted_for_tenant1" + + # Encrypt token for tenant1 + encrypted = encrypt_token("tenant-123", "sensitive_data") + + # Attempt to decrypt with different tenant should fail + with patch("libs.rsa.decrypt") as mock_decrypt: + mock_decrypt.side_effect = Exception("Invalid tenant key") + + with pytest.raises(Exception, match="Invalid tenant key"): + decrypt_token("different-tenant", encrypted) + + @patch("libs.rsa.decrypt") + def test_tampered_ciphertext_rejection(self, mock_decrypt): + """Detect and reject tampered ciphertext""" + valid_encrypted = base64.b64encode(b"valid_data").decode() + + # Tamper with ciphertext + tampered_bytes = bytearray(base64.b64decode(valid_encrypted)) + tampered_bytes[0] ^= 0xFF + tampered = base64.b64encode(bytes(tampered_bytes)).decode() + + mock_decrypt.side_effect = Exception("Decryption error") + + with pytest.raises(Exception, match="Decryption error"): + decrypt_token("tenant-123", tampered) + + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_encryption_randomness(self, mock_encrypt, mock_query): + """Ensure same plaintext produces different ciphertext""" + mock_tenant = MagicMock(encrypt_public_key="key") + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + + # Different outputs for same input + mock_encrypt.side_effect = [b"enc1", b"enc2", b"enc3"] + + results = [encrypt_token("tenant-123", "token") for _ in range(3)] + + # All results should be different + assert len(set(results)) == 3 + + +class TestEdgeCases: + """Additional security-focused edge case tests""" + + def test_should_handle_empty_string_in_obfuscation(self): + """Test handling of empty string in obfuscation""" + # Test empty string (which is a valid str type) + assert obfuscated_token("") == "" + + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_should_handle_empty_token_encryption(self, mock_encrypt, mock_query): + """Test encryption of empty token""" + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "mock_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + mock_encrypt.return_value = b"encrypted_empty" + + result = encrypt_token("tenant-123", "") + + assert result == base64.b64encode(b"encrypted_empty").decode() + mock_encrypt.assert_called_with("", "mock_public_key") + + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_should_handle_special_characters_in_token(self, mock_encrypt, mock_query): + """Test tokens containing special/unicode characters""" + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "mock_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + mock_encrypt.return_value = b"encrypted_special" + + # Test various special characters + special_tokens = [ + "token\x00with\x00null", # Null bytes + "token_with_emoji_😀🎉", # Unicode emoji + "token\nwith\nnewlines", # Newlines + "token\twith\ttabs", # Tabs + "token_with_中文字符", # Chinese characters + ] + + for token in special_tokens: + result = encrypt_token("tenant-123", token) + assert result == base64.b64encode(b"encrypted_special").decode() + mock_encrypt.assert_called_with(token, "mock_public_key") + + @patch("models.engine.db.session.query") + @patch("libs.rsa.encrypt") + def test_should_handle_rsa_size_limits(self, mock_encrypt, mock_query): + """Test behavior when token exceeds RSA encryption limits""" + mock_tenant = MagicMock() + mock_tenant.encrypt_public_key = "mock_public_key" + mock_query.return_value.filter.return_value.first.return_value = mock_tenant + + # RSA 2048-bit can only encrypt ~245 bytes + # The actual limit depends on padding scheme + mock_encrypt.side_effect = ValueError("Message too long for RSA key size") + + # Create a token that would exceed RSA limits + long_token = "x" * 300 + + with pytest.raises(ValueError, match="Message too long for RSA key size"): + encrypt_token("tenant-123", long_token) + + @patch("libs.rsa.get_decrypt_decoding") + @patch("libs.rsa.decrypt_token_with_decoding") + def test_batch_decrypt_loads_key_only_once(self, mock_decrypt_with_decoding, mock_get_decoding): + """Verify batch decryption optimization - loads key only once""" + mock_rsa_key = MagicMock() + mock_cipher_rsa = MagicMock() + mock_get_decoding.return_value = (mock_rsa_key, mock_cipher_rsa) + + # Test with multiple tokens + mock_decrypt_with_decoding.side_effect = ["token1", "token2", "token3", "token4", "token5"] + tokens = [base64.b64encode(f"encrypted{i}".encode()).decode() for i in range(5)] + + result = batch_decrypt_token("tenant-123", tokens) + + assert result == ["token1", "token2", "token3", "token4", "token5"] + # Key should only be loaded once regardless of token count + mock_get_decoding.assert_called_once_with("tenant-123") + assert mock_decrypt_with_decoding.call_count == 5 diff --git a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py index c688d3952b..37749f0c66 100644 --- a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py +++ b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py @@ -1,4 +1,4 @@ -import random +import secrets from unittest.mock import MagicMock, patch import pytest @@ -34,7 +34,7 @@ def test_retry_logic_success(mock_request): side_effects = [] for _ in range(SSRF_DEFAULT_MAX_RETRIES): - status_code = random.choice(STATUS_FORCELIST) + status_code = secrets.choice(STATUS_FORCELIST) mock_response = MagicMock() mock_response.status_code = status_code side_effects.append(mock_response) diff --git a/api/tests/unit_tests/core/helper/test_url_signer.py b/api/tests/unit_tests/core/helper/test_url_signer.py new file mode 100644 index 0000000000..5af24777de --- /dev/null +++ b/api/tests/unit_tests/core/helper/test_url_signer.py @@ -0,0 +1,194 @@ +from unittest.mock import patch +from urllib.parse import parse_qs, urlparse + +import pytest + +from core.helper.url_signer import SignedUrlParams, UrlSigner + + +class TestUrlSigner: + """Test cases for UrlSigner class""" + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_generate_signed_url_params(self): + """Test generation of signed URL parameters with all required fields""" + sign_key = "test-sign-key" + prefix = "test-prefix" + + params = UrlSigner.get_signed_url_params(sign_key, prefix) + + # Verify the returned object and required fields + assert isinstance(params, SignedUrlParams) + assert params.sign_key == sign_key + assert params.timestamp is not None + assert params.nonce is not None + assert params.sign is not None + + # Verify nonce format (32 character hex string) + assert len(params.nonce) == 32 + assert all(c in "0123456789abcdef" for c in params.nonce) + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_generate_complete_signed_url(self): + """Test generation of complete signed URL with query parameters""" + base_url = "https://example.com/api/test" + sign_key = "test-sign-key" + prefix = "test-prefix" + + signed_url = UrlSigner.get_signed_url(base_url, sign_key, prefix) + + # Parse URL and verify structure + parsed = urlparse(signed_url) + assert f"{parsed.scheme}://{parsed.netloc}{parsed.path}" == base_url + + # Verify query parameters + query_params = parse_qs(parsed.query) + assert "timestamp" in query_params + assert "nonce" in query_params + assert "sign" in query_params + + # Verify each parameter has exactly one value + assert len(query_params["timestamp"]) == 1 + assert len(query_params["nonce"]) == 1 + assert len(query_params["sign"]) == 1 + + # Verify parameter values are not empty + assert query_params["timestamp"][0] + assert query_params["nonce"][0] + assert query_params["sign"][0] + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_verify_valid_signature(self): + """Test verification of valid signature""" + sign_key = "test-sign-key" + prefix = "test-prefix" + + # Generate and verify signature + params = UrlSigner.get_signed_url_params(sign_key, prefix) + + is_valid = UrlSigner.verify( + sign_key=sign_key, timestamp=params.timestamp, nonce=params.nonce, sign=params.sign, prefix=prefix + ) + + assert is_valid is True + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + @pytest.mark.parametrize( + ("field", "modifier"), + [ + ("sign_key", lambda _: "wrong-sign-key"), + ("timestamp", lambda t: str(int(t) + 1000)), + ("nonce", lambda _: "different-nonce-123456789012345"), + ("prefix", lambda _: "wrong-prefix"), + ("sign", lambda s: s + "tampered"), + ], + ) + def test_should_reject_invalid_signature_params(self, field, modifier): + """Test signature verification rejects invalid parameters""" + sign_key = "test-sign-key" + prefix = "test-prefix" + + # Generate valid signed parameters + params = UrlSigner.get_signed_url_params(sign_key, prefix) + + # Prepare verification parameters + verify_params = { + "sign_key": sign_key, + "timestamp": params.timestamp, + "nonce": params.nonce, + "sign": params.sign, + "prefix": prefix, + } + + # Modify the specific field + verify_params[field] = modifier(verify_params[field]) + + # Verify should fail + is_valid = UrlSigner.verify(**verify_params) + assert is_valid is False + + @patch("configs.dify_config.SECRET_KEY", None) + def test_should_raise_error_without_secret_key(self): + """Test that signing fails when SECRET_KEY is not configured""" + with pytest.raises(Exception) as exc_info: + UrlSigner.get_signed_url_params("key", "prefix") + + assert "SECRET_KEY is not set" in str(exc_info.value) + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_generate_unique_signatures(self): + """Test that different inputs produce different signatures""" + params1 = UrlSigner.get_signed_url_params("key1", "prefix1") + params2 = UrlSigner.get_signed_url_params("key2", "prefix2") + + # Different inputs should produce different signatures + assert params1.sign != params2.sign + assert params1.nonce != params2.nonce + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_handle_special_characters(self): + """Test handling of special characters in parameters""" + special_cases = [ + "test with spaces", + "test/with/slashes", + "test中文字符", + ] + + for sign_key in special_cases: + params = UrlSigner.get_signed_url_params(sign_key, "prefix") + + # Should generate valid signature and verify correctly + is_valid = UrlSigner.verify( + sign_key=sign_key, timestamp=params.timestamp, nonce=params.nonce, sign=params.sign, prefix="prefix" + ) + assert is_valid is True + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_ensure_nonce_randomness(self): + """Test that nonce is random for each generation - critical for security""" + sign_key = "test-sign-key" + prefix = "test-prefix" + + # Generate multiple nonces + nonces = set() + for _ in range(5): + params = UrlSigner.get_signed_url_params(sign_key, prefix) + nonces.add(params.nonce) + + # All nonces should be unique + assert len(nonces) == 5 + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + @patch("time.time", return_value=1234567890) + @patch("os.urandom", return_value=b"\xab\xcd\xef\x12\x34\x56\x78\x90\xab\xcd\xef\x12\x34\x56\x78\x90") + def test_should_produce_consistent_signatures(self, mock_urandom, mock_time): + """Test that same inputs produce same signature - ensures deterministic behavior""" + sign_key = "test-sign-key" + prefix = "test-prefix" + + # Generate signature multiple times with same inputs (time and nonce are mocked) + params1 = UrlSigner.get_signed_url_params(sign_key, prefix) + params2 = UrlSigner.get_signed_url_params(sign_key, prefix) + + # With mocked time and random, should produce identical results + assert params1.timestamp == params2.timestamp + assert params1.nonce == params2.nonce + assert params1.sign == params2.sign + + # Verify the signature is valid + assert UrlSigner.verify( + sign_key=sign_key, timestamp=params1.timestamp, nonce=params1.nonce, sign=params1.sign, prefix=prefix + ) + + @patch("configs.dify_config.SECRET_KEY", "test-secret-key-12345") + def test_should_handle_empty_strings(self): + """Test handling of empty string parameters - common edge case""" + # Empty sign_key and prefix should still work + params = UrlSigner.get_signed_url_params("", "") + assert params.sign is not None + + # Should verify correctly + is_valid = UrlSigner.verify( + sign_key="", timestamp=params.timestamp, nonce=params.nonce, sign=params.sign, prefix="" + ) + assert is_valid is True diff --git a/api/tests/unit_tests/core/mcp/client/test_session.py b/api/tests/unit_tests/core/mcp/client/test_session.py new file mode 100644 index 0000000000..c84169bf15 --- /dev/null +++ b/api/tests/unit_tests/core/mcp/client/test_session.py @@ -0,0 +1,471 @@ +import queue +import threading +from typing import Any + +from core.mcp import types +from core.mcp.entities import RequestContext +from core.mcp.session.base_session import RequestResponder +from core.mcp.session.client_session import DEFAULT_CLIENT_INFO, ClientSession +from core.mcp.types import ( + LATEST_PROTOCOL_VERSION, + ClientNotification, + ClientRequest, + Implementation, + InitializedNotification, + InitializeRequest, + InitializeResult, + JSONRPCMessage, + JSONRPCNotification, + JSONRPCRequest, + JSONRPCResponse, + ServerCapabilities, + ServerResult, + SessionMessage, +) + + +def test_client_session_initialize(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + initialized_notification = None + + def mock_server(): + nonlocal initialized_notification + + # Receive initialization request + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + + # Create response + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities( + logging=None, + resources=None, + tools=None, + experimental=None, + prompts=None, + ), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + instructions="The server instructions.", + ) + ) + + # Send response + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + + # Receive initialized notification + session_notification = client_to_server.get(timeout=5.0) + jsonrpc_notification = session_notification.message + assert isinstance(jsonrpc_notification.root, JSONRPCNotification) + initialized_notification = ClientNotification.model_validate( + jsonrpc_notification.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + + # Create message handler + def message_handler( + message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception, + ) -> None: + if isinstance(message, Exception): + raise message + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + # Create and use client session + with ClientSession( + server_to_client, + client_to_server, + message_handler=message_handler, + ) as session: + result = session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Assert results + assert isinstance(result, InitializeResult) + assert result.protocolVersion == LATEST_PROTOCOL_VERSION + assert isinstance(result.capabilities, ServerCapabilities) + assert result.serverInfo == Implementation(name="mock-server", version="0.1.0") + assert result.instructions == "The server instructions." + + # Check that client sent initialized notification + assert initialized_notification + assert isinstance(initialized_notification.root, InitializedNotification) + + +def test_client_session_custom_client_info(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + custom_client_info = Implementation(name="test-client", version="1.2.3") + received_client_info = None + + def mock_server(): + nonlocal received_client_info + + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + received_client_info = request.root.params.clientInfo + + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + # Receive initialized notification + client_to_server.get(timeout=5.0) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + client_info=custom_client_info, + ) as session: + session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Assert that custom client info was sent + assert received_client_info == custom_client_info + + +def test_client_session_default_client_info(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + received_client_info = None + + def mock_server(): + nonlocal received_client_info + + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + received_client_info = request.root.params.clientInfo + + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + # Receive initialized notification + client_to_server.get(timeout=5.0) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + ) as session: + session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Assert that default client info was used + assert received_client_info == DEFAULT_CLIENT_INFO + + +def test_client_session_version_negotiation_success(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + def mock_server(): + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + + # Send supported protocol version + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + # Receive initialized notification + client_to_server.get(timeout=5.0) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + ) as session: + result = session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Should successfully initialize + assert isinstance(result, InitializeResult) + assert result.protocolVersion == LATEST_PROTOCOL_VERSION + + +def test_client_session_version_negotiation_failure(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + def mock_server(): + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + + # Send unsupported protocol version + result = ServerResult( + InitializeResult( + protocolVersion="99.99.99", # Unsupported version + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + ) as session: + import pytest + + with pytest.raises(RuntimeError, match="Unsupported protocol version"): + session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + +def test_client_capabilities_default(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + received_capabilities = None + + def mock_server(): + nonlocal received_capabilities + + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + received_capabilities = request.root.params.capabilities + + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + # Receive initialized notification + client_to_server.get(timeout=5.0) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + ) as session: + session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Assert default capabilities + assert received_capabilities is not None + assert received_capabilities.sampling is not None + assert received_capabilities.roots is not None + assert received_capabilities.roots.listChanged is True + + +def test_client_capabilities_with_custom_callbacks(): + # Create synchronous queues to replace async streams + client_to_server: queue.Queue[SessionMessage] = queue.Queue() + server_to_client: queue.Queue[SessionMessage] = queue.Queue() + + def custom_sampling_callback( + context: RequestContext["ClientSession", Any], + params: types.CreateMessageRequestParams, + ) -> types.CreateMessageResult | types.ErrorData: + return types.CreateMessageResult( + model="test-model", + role="assistant", + content=types.TextContent(type="text", text="Custom response"), + ) + + def custom_list_roots_callback( + context: RequestContext["ClientSession", Any], + ) -> types.ListRootsResult | types.ErrorData: + return types.ListRootsResult(roots=[]) + + def mock_server(): + session_message = client_to_server.get(timeout=5.0) + jsonrpc_request = session_message.message + assert isinstance(jsonrpc_request.root, JSONRPCRequest) + request = ClientRequest.model_validate( + jsonrpc_request.root.model_dump(by_alias=True, mode="json", exclude_none=True) + ) + assert isinstance(request.root, InitializeRequest) + + result = ServerResult( + InitializeResult( + protocolVersion=LATEST_PROTOCOL_VERSION, + capabilities=ServerCapabilities(), + serverInfo=Implementation(name="mock-server", version="0.1.0"), + ) + ) + + server_to_client.put( + SessionMessage( + message=JSONRPCMessage( + JSONRPCResponse( + jsonrpc="2.0", + id=jsonrpc_request.root.id, + result=result.model_dump(by_alias=True, mode="json", exclude_none=True), + ) + ) + ) + ) + # Receive initialized notification + client_to_server.get(timeout=5.0) + + # Start mock server thread + server_thread = threading.Thread(target=mock_server, daemon=True) + server_thread.start() + + with ClientSession( + server_to_client, + client_to_server, + sampling_callback=custom_sampling_callback, + list_roots_callback=custom_list_roots_callback, + ) as session: + result = session.initialize() + + # Wait for server thread to complete + server_thread.join(timeout=10.0) + + # Verify initialization succeeded + assert isinstance(result, InitializeResult) + assert result.protocolVersion == LATEST_PROTOCOL_VERSION diff --git a/api/tests/unit_tests/core/mcp/client/test_sse.py b/api/tests/unit_tests/core/mcp/client/test_sse.py new file mode 100644 index 0000000000..8122cd08eb --- /dev/null +++ b/api/tests/unit_tests/core/mcp/client/test_sse.py @@ -0,0 +1,349 @@ +import json +import queue +import threading +import time +from typing import Any +from unittest.mock import Mock, patch + +import httpx +import pytest + +from core.mcp import types +from core.mcp.client.sse_client import sse_client +from core.mcp.error import MCPAuthError, MCPConnectionError + +SERVER_NAME = "test_server_for_SSE" + + +def test_sse_message_id_coercion(): + """Test that string message IDs that look like integers are parsed as integers. + + See for more details. + """ + json_message = '{"jsonrpc": "2.0", "id": "123", "method": "ping", "params": null}' + msg = types.JSONRPCMessage.model_validate_json(json_message) + expected = types.JSONRPCMessage(root=types.JSONRPCRequest(method="ping", jsonrpc="2.0", id=123)) + + # Check if both are JSONRPCRequest instances + assert isinstance(msg.root, types.JSONRPCRequest) + assert isinstance(expected.root, types.JSONRPCRequest) + + assert msg.root.id == expected.root.id + assert msg.root.method == expected.root.method + assert msg.root.jsonrpc == expected.root.jsonrpc + + +class MockSSEClient: + """Mock SSE client for testing.""" + + def __init__(self, url: str, headers: dict[str, Any] | None = None): + self.url = url + self.headers = headers or {} + self.connected = False + self.read_queue: queue.Queue = queue.Queue() + self.write_queue: queue.Queue = queue.Queue() + + def connect(self): + """Simulate connection establishment.""" + self.connected = True + + # Send endpoint event + endpoint_data = "/messages/?session_id=test-session-123" + self.read_queue.put(("endpoint", endpoint_data)) + + return self.read_queue, self.write_queue + + def send_initialize_response(self): + """Send a mock initialize response.""" + response = { + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": types.LATEST_PROTOCOL_VERSION, + "capabilities": { + "logging": None, + "resources": None, + "tools": None, + "experimental": None, + "prompts": None, + }, + "serverInfo": {"name": SERVER_NAME, "version": "0.1.0"}, + "instructions": "Test server instructions.", + }, + } + self.read_queue.put(("message", json.dumps(response))) + + +def test_sse_client_message_id_handling(): + """Test SSE client properly handles message ID coercion.""" + mock_client = MockSSEClient("http://test.example/sse") + read_queue, write_queue = mock_client.connect() + + # Send a message with string ID that should be coerced to int + message_data = { + "jsonrpc": "2.0", + "id": "456", # String ID + "result": {"test": "data"}, + } + read_queue.put(("message", json.dumps(message_data))) + read_queue.get(timeout=1.0) + # Get the message from queue + event_type, data = read_queue.get(timeout=1.0) + assert event_type == "message" + + # Parse the message + parsed_message = types.JSONRPCMessage.model_validate_json(data) + # Check that it's a JSONRPCResponse and verify the ID + assert isinstance(parsed_message.root, types.JSONRPCResponse) + assert parsed_message.root.id == 456 # Should be converted to int + + +def test_sse_client_connection_validation(): + """Test SSE client validates endpoint URLs properly.""" + test_url = "http://test.example/sse" + + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock the HTTP client + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + # Mock the SSE connection + mock_event_source = Mock() + mock_event_source.response.raise_for_status.return_value = None + mock_sse_connect.return_value.__enter__.return_value = mock_event_source + + # Mock SSE events + class MockSSEEvent: + def __init__(self, event_type: str, data: str): + self.event = event_type + self.data = data + + # Simulate endpoint event + endpoint_event = MockSSEEvent("endpoint", "/messages/?session_id=test-123") + mock_event_source.iter_sse.return_value = [endpoint_event] + + # Test connection + try: + with sse_client(test_url) as (read_queue, write_queue): + assert read_queue is not None + assert write_queue is not None + except Exception as e: + # Connection might fail due to mocking, but we're testing the validation logic + pass + + +def test_sse_client_error_handling(): + """Test SSE client properly handles various error conditions.""" + test_url = "http://test.example/sse" + + # Test 401 error handling + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock 401 HTTP error + mock_error = httpx.HTTPStatusError("Unauthorized", request=Mock(), response=Mock(status_code=401)) + mock_sse_connect.side_effect = mock_error + + with pytest.raises(MCPAuthError): + with sse_client(test_url): + pass + + # Test other HTTP errors + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock other HTTP error + mock_error = httpx.HTTPStatusError("Server Error", request=Mock(), response=Mock(status_code=500)) + mock_sse_connect.side_effect = mock_error + + with pytest.raises(MCPConnectionError): + with sse_client(test_url): + pass + + +def test_sse_client_timeout_configuration(): + """Test SSE client timeout configuration.""" + test_url = "http://test.example/sse" + custom_timeout = 10.0 + custom_sse_timeout = 300.0 + custom_headers = {"Authorization": "Bearer test-token"} + + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock successful connection + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + mock_event_source = Mock() + mock_event_source.response.raise_for_status.return_value = None + mock_event_source.iter_sse.return_value = [] + mock_sse_connect.return_value.__enter__.return_value = mock_event_source + + try: + with sse_client( + test_url, headers=custom_headers, timeout=custom_timeout, sse_read_timeout=custom_sse_timeout + ) as (read_queue, write_queue): + # Verify the configuration was passed correctly + mock_client_factory.assert_called_with(headers=custom_headers) + + # Check that timeout was configured + call_args = mock_sse_connect.call_args + assert call_args is not None + timeout_arg = call_args[1]["timeout"] + assert timeout_arg.read == custom_sse_timeout + except Exception: + # Connection might fail due to mocking, but we tested the configuration + pass + + +def test_sse_transport_endpoint_validation(): + """Test SSE transport validates endpoint URLs correctly.""" + from core.mcp.client.sse_client import SSETransport + + transport = SSETransport("http://example.com/sse") + + # Valid endpoint (same origin) + valid_endpoint = "http://example.com/messages/session123" + assert transport._validate_endpoint_url(valid_endpoint) == True + + # Invalid endpoint (different origin) + invalid_endpoint = "http://malicious.com/messages/session123" + assert transport._validate_endpoint_url(invalid_endpoint) == False + + # Invalid endpoint (different scheme) + invalid_scheme = "https://example.com/messages/session123" + assert transport._validate_endpoint_url(invalid_scheme) == False + + +def test_sse_transport_message_parsing(): + """Test SSE transport properly parses different message types.""" + from core.mcp.client.sse_client import SSETransport + + transport = SSETransport("http://example.com/sse") + read_queue: queue.Queue = queue.Queue() + + # Test valid JSON-RPC message + valid_message = '{"jsonrpc": "2.0", "id": 1, "method": "ping"}' + transport._handle_message_event(valid_message, read_queue) + + # Should have a SessionMessage in the queue + message = read_queue.get(timeout=1.0) + assert message is not None + assert hasattr(message, "message") + + # Test invalid JSON + invalid_json = '{"invalid": json}' + transport._handle_message_event(invalid_json, read_queue) + + # Should have an exception in the queue + error = read_queue.get(timeout=1.0) + assert isinstance(error, Exception) + + +def test_sse_client_queue_cleanup(): + """Test that SSE client properly cleans up queues on exit.""" + test_url = "http://test.example/sse" + + read_queue = None + write_queue = None + + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock connection that raises an exception + mock_sse_connect.side_effect = Exception("Connection failed") + + try: + with sse_client(test_url) as (rq, wq): + read_queue = rq + write_queue = wq + except Exception: + pass # Expected to fail + + # Queues should be cleaned up even on exception + # Note: In real implementation, cleanup should put None to signal shutdown + + +def test_sse_client_url_processing(): + """Test SSE client URL processing functions.""" + from core.mcp.client.sse_client import remove_request_params + + # Test URL with parameters + url_with_params = "http://example.com/sse?param1=value1¶m2=value2" + cleaned_url = remove_request_params(url_with_params) + assert cleaned_url == "http://example.com/sse" + + # Test URL without parameters + url_without_params = "http://example.com/sse" + cleaned_url = remove_request_params(url_without_params) + assert cleaned_url == "http://example.com/sse" + + # Test URL with path and parameters + complex_url = "http://example.com/path/to/sse?session=123&token=abc" + cleaned_url = remove_request_params(complex_url) + assert cleaned_url == "http://example.com/path/to/sse" + + +def test_sse_client_headers_propagation(): + """Test that custom headers are properly propagated in SSE client.""" + test_url = "http://test.example/sse" + custom_headers = { + "Authorization": "Bearer test-token", + "X-Custom-Header": "test-value", + "User-Agent": "test-client/1.0", + } + + with patch("core.mcp.client.sse_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + with patch("core.mcp.client.sse_client.ssrf_proxy_sse_connect") as mock_sse_connect: + # Mock the client factory to capture headers + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + # Mock the SSE connection + mock_event_source = Mock() + mock_event_source.response.raise_for_status.return_value = None + mock_event_source.iter_sse.return_value = [] + mock_sse_connect.return_value.__enter__.return_value = mock_event_source + + try: + with sse_client(test_url, headers=custom_headers): + pass + except Exception: + pass # Expected due to mocking + + # Verify headers were passed to client factory + mock_client_factory.assert_called_with(headers=custom_headers) + + +def test_sse_client_concurrent_access(): + """Test SSE client behavior with concurrent queue access.""" + test_read_queue: queue.Queue = queue.Queue() + + # Simulate concurrent producers and consumers + def producer(): + for i in range(10): + test_read_queue.put(f"message_{i}") + time.sleep(0.01) # Small delay to simulate real conditions + + def consumer(): + received = [] + for _ in range(10): + try: + msg = test_read_queue.get(timeout=2.0) + received.append(msg) + except queue.Empty: + break + return received + + # Start producer in separate thread + producer_thread = threading.Thread(target=producer, daemon=True) + producer_thread.start() + + # Consume messages + received_messages = consumer() + + # Wait for producer to finish + producer_thread.join(timeout=5.0) + + # Verify all messages were received + assert len(received_messages) == 10 + for i in range(10): + assert f"message_{i}" in received_messages diff --git a/api/tests/unit_tests/core/mcp/client/test_streamable_http.py b/api/tests/unit_tests/core/mcp/client/test_streamable_http.py new file mode 100644 index 0000000000..9a30a35a49 --- /dev/null +++ b/api/tests/unit_tests/core/mcp/client/test_streamable_http.py @@ -0,0 +1,450 @@ +""" +Tests for the StreamableHTTP client transport. + +Contains tests for only the client side of the StreamableHTTP transport. +""" + +import queue +import threading +import time +from typing import Any +from unittest.mock import Mock, patch + +from core.mcp import types +from core.mcp.client.streamable_client import streamablehttp_client + +# Test constants +SERVER_NAME = "test_streamable_http_server" +TEST_SESSION_ID = "test-session-id-12345" +INIT_REQUEST = { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "clientInfo": {"name": "test-client", "version": "1.0"}, + "protocolVersion": "2025-03-26", + "capabilities": {}, + }, + "id": "init-1", +} + + +class MockStreamableHTTPClient: + """Mock StreamableHTTP client for testing.""" + + def __init__(self, url: str, headers: dict[str, Any] | None = None): + self.url = url + self.headers = headers or {} + self.connected = False + self.read_queue: queue.Queue = queue.Queue() + self.write_queue: queue.Queue = queue.Queue() + self.session_id = TEST_SESSION_ID + + def connect(self): + """Simulate connection establishment.""" + self.connected = True + return self.read_queue, self.write_queue, lambda: self.session_id + + def send_initialize_response(self): + """Send a mock initialize response.""" + session_message = types.SessionMessage( + message=types.JSONRPCMessage( + root=types.JSONRPCResponse( + jsonrpc="2.0", + id="init-1", + result={ + "protocolVersion": types.LATEST_PROTOCOL_VERSION, + "capabilities": { + "logging": None, + "resources": None, + "tools": None, + "experimental": None, + "prompts": None, + }, + "serverInfo": {"name": SERVER_NAME, "version": "0.1.0"}, + "instructions": "Test server instructions.", + }, + ) + ) + ) + self.read_queue.put(session_message) + + def send_tools_response(self): + """Send a mock tools list response.""" + session_message = types.SessionMessage( + message=types.JSONRPCMessage( + root=types.JSONRPCResponse( + jsonrpc="2.0", + id="tools-1", + result={ + "tools": [ + { + "name": "test_tool", + "description": "A test tool", + "inputSchema": {"type": "object", "properties": {}}, + } + ], + }, + ) + ) + ) + self.read_queue.put(session_message) + + +def test_streamablehttp_client_message_id_handling(): + """Test StreamableHTTP client properly handles message ID coercion.""" + mock_client = MockStreamableHTTPClient("http://test.example/mcp") + read_queue, write_queue, get_session_id = mock_client.connect() + + # Send a message with string ID that should be coerced to int + response_message = types.SessionMessage( + message=types.JSONRPCMessage(root=types.JSONRPCResponse(jsonrpc="2.0", id="789", result={"test": "data"})) + ) + read_queue.put(response_message) + + # Get the message from queue + message = read_queue.get(timeout=1.0) + assert message is not None + assert isinstance(message, types.SessionMessage) + + # Check that the ID was properly handled + assert isinstance(message.message.root, types.JSONRPCResponse) + assert message.message.root.id == 789 # ID should be coerced to int due to union_mode="left_to_right" + + +def test_streamablehttp_client_connection_validation(): + """Test StreamableHTTP client validates connections properly.""" + test_url = "http://test.example/mcp" + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + # Mock the HTTP client + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + # Mock successful response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.raise_for_status.return_value = None + mock_client.post.return_value = mock_response + + # Test connection + try: + with streamablehttp_client(test_url) as (read_queue, write_queue, get_session_id): + assert read_queue is not None + assert write_queue is not None + assert get_session_id is not None + except Exception: + # Connection might fail due to mocking, but we're testing the validation logic + pass + + +def test_streamablehttp_client_timeout_configuration(): + """Test StreamableHTTP client timeout configuration.""" + test_url = "http://test.example/mcp" + custom_headers = {"Authorization": "Bearer test-token"} + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + # Mock successful connection + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.raise_for_status.return_value = None + mock_client.post.return_value = mock_response + + try: + with streamablehttp_client(test_url, headers=custom_headers) as (read_queue, write_queue, get_session_id): + # Verify the configuration was passed correctly + mock_client_factory.assert_called_with(headers=custom_headers) + except Exception: + # Connection might fail due to mocking, but we tested the configuration + pass + + +def test_streamablehttp_client_session_id_handling(): + """Test StreamableHTTP client properly handles session IDs.""" + mock_client = MockStreamableHTTPClient("http://test.example/mcp") + read_queue, write_queue, get_session_id = mock_client.connect() + + # Test that session ID is available + session_id = get_session_id() + assert session_id == TEST_SESSION_ID + + # Test that we can use the session ID in subsequent requests + assert session_id is not None + assert len(session_id) > 0 + + +def test_streamablehttp_client_message_parsing(): + """Test StreamableHTTP client properly parses different message types.""" + mock_client = MockStreamableHTTPClient("http://test.example/mcp") + read_queue, write_queue, get_session_id = mock_client.connect() + + # Test valid initialization response + mock_client.send_initialize_response() + + # Should have a SessionMessage in the queue + message = read_queue.get(timeout=1.0) + assert message is not None + assert isinstance(message, types.SessionMessage) + assert isinstance(message.message.root, types.JSONRPCResponse) + + # Test tools response + mock_client.send_tools_response() + + tools_message = read_queue.get(timeout=1.0) + assert tools_message is not None + assert isinstance(tools_message, types.SessionMessage) + + +def test_streamablehttp_client_queue_cleanup(): + """Test that StreamableHTTP client properly cleans up queues on exit.""" + test_url = "http://test.example/mcp" + + read_queue = None + write_queue = None + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + # Mock connection that raises an exception + mock_client_factory.side_effect = Exception("Connection failed") + + try: + with streamablehttp_client(test_url) as (rq, wq, get_session_id): + read_queue = rq + write_queue = wq + except Exception: + pass # Expected to fail + + # Queues should be cleaned up even on exception + # Note: In real implementation, cleanup should put None to signal shutdown + + +def test_streamablehttp_client_headers_propagation(): + """Test that custom headers are properly propagated in StreamableHTTP client.""" + test_url = "http://test.example/mcp" + custom_headers = { + "Authorization": "Bearer test-token", + "X-Custom-Header": "test-value", + "User-Agent": "test-client/1.0", + } + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + # Mock the client factory to capture headers + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.raise_for_status.return_value = None + mock_client.post.return_value = mock_response + + try: + with streamablehttp_client(test_url, headers=custom_headers): + pass + except Exception: + pass # Expected due to mocking + + # Verify headers were passed to client factory + # Check that the call was made with headers that include our custom headers + mock_client_factory.assert_called_once() + call_args = mock_client_factory.call_args + assert "headers" in call_args.kwargs + passed_headers = call_args.kwargs["headers"] + + # Verify all custom headers are present + for key, value in custom_headers.items(): + assert key in passed_headers + assert passed_headers[key] == value + + +def test_streamablehttp_client_concurrent_access(): + """Test StreamableHTTP client behavior with concurrent queue access.""" + test_read_queue: queue.Queue = queue.Queue() + test_write_queue: queue.Queue = queue.Queue() + + # Simulate concurrent producers and consumers + def producer(): + for i in range(10): + test_read_queue.put(f"message_{i}") + time.sleep(0.01) # Small delay to simulate real conditions + + def consumer(): + received = [] + for _ in range(10): + try: + msg = test_read_queue.get(timeout=2.0) + received.append(msg) + except queue.Empty: + break + return received + + # Start producer in separate thread + producer_thread = threading.Thread(target=producer, daemon=True) + producer_thread.start() + + # Consume messages + received_messages = consumer() + + # Wait for producer to finish + producer_thread.join(timeout=5.0) + + # Verify all messages were received + assert len(received_messages) == 10 + for i in range(10): + assert f"message_{i}" in received_messages + + +def test_streamablehttp_client_json_vs_sse_mode(): + """Test StreamableHTTP client handling of JSON vs SSE response modes.""" + test_url = "http://test.example/mcp" + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + # Mock JSON response + mock_json_response = Mock() + mock_json_response.status_code = 200 + mock_json_response.headers = {"content-type": "application/json"} + mock_json_response.json.return_value = {"result": "json_mode"} + mock_json_response.raise_for_status.return_value = None + + # Mock SSE response + mock_sse_response = Mock() + mock_sse_response.status_code = 200 + mock_sse_response.headers = {"content-type": "text/event-stream"} + mock_sse_response.raise_for_status.return_value = None + + # Test JSON mode + mock_client.post.return_value = mock_json_response + + try: + with streamablehttp_client(test_url) as (read_queue, write_queue, get_session_id): + # Should handle JSON responses + assert read_queue is not None + assert write_queue is not None + except Exception: + pass # Expected due to mocking + + # Test SSE mode + mock_client.post.return_value = mock_sse_response + + try: + with streamablehttp_client(test_url) as (read_queue, write_queue, get_session_id): + # Should handle SSE responses + assert read_queue is not None + assert write_queue is not None + except Exception: + pass # Expected due to mocking + + +def test_streamablehttp_client_terminate_on_close(): + """Test StreamableHTTP client terminate_on_close parameter.""" + test_url = "http://test.example/mcp" + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.raise_for_status.return_value = None + mock_client.post.return_value = mock_response + mock_client.delete.return_value = mock_response + + # Test with terminate_on_close=True (default) + try: + with streamablehttp_client(test_url, terminate_on_close=True) as (read_queue, write_queue, get_session_id): + pass + except Exception: + pass # Expected due to mocking + + # Test with terminate_on_close=False + try: + with streamablehttp_client(test_url, terminate_on_close=False) as (read_queue, write_queue, get_session_id): + pass + except Exception: + pass # Expected due to mocking + + +def test_streamablehttp_client_protocol_version_handling(): + """Test StreamableHTTP client protocol version handling.""" + mock_client = MockStreamableHTTPClient("http://test.example/mcp") + read_queue, write_queue, get_session_id = mock_client.connect() + + # Send initialize response with specific protocol version + + session_message = types.SessionMessage( + message=types.JSONRPCMessage( + root=types.JSONRPCResponse( + jsonrpc="2.0", + id="init-1", + result={ + "protocolVersion": "2024-11-05", + "capabilities": {}, + "serverInfo": {"name": SERVER_NAME, "version": "0.1.0"}, + }, + ) + ) + ) + read_queue.put(session_message) + + # Get the message and verify protocol version + message = read_queue.get(timeout=1.0) + assert message is not None + assert isinstance(message.message.root, types.JSONRPCResponse) + result = message.message.root.result + assert result["protocolVersion"] == "2024-11-05" + + +def test_streamablehttp_client_error_response_handling(): + """Test StreamableHTTP client handling of error responses.""" + mock_client = MockStreamableHTTPClient("http://test.example/mcp") + read_queue, write_queue, get_session_id = mock_client.connect() + + # Send an error response + session_message = types.SessionMessage( + message=types.JSONRPCMessage( + root=types.JSONRPCError( + jsonrpc="2.0", + id="test-1", + error=types.ErrorData(code=-32601, message="Method not found", data=None), + ) + ) + ) + read_queue.put(session_message) + + # Get the error message + message = read_queue.get(timeout=1.0) + assert message is not None + assert isinstance(message.message.root, types.JSONRPCError) + assert message.message.root.error.code == -32601 + assert message.message.root.error.message == "Method not found" + + +def test_streamablehttp_client_resumption_token_handling(): + """Test StreamableHTTP client resumption token functionality.""" + test_url = "http://test.example/mcp" + test_resumption_token = "resume-token-123" + + with patch("core.mcp.client.streamable_client.create_ssrf_proxy_mcp_http_client") as mock_client_factory: + mock_client = Mock() + mock_client_factory.return_value.__enter__.return_value = mock_client + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json", "last-event-id": test_resumption_token} + mock_response.raise_for_status.return_value = None + mock_client.post.return_value = mock_response + + try: + with streamablehttp_client(test_url) as (read_queue, write_queue, get_session_id): + # Test that resumption token can be captured from headers + assert read_queue is not None + assert write_queue is not None + except Exception: + pass # Expected due to mocking diff --git a/api/tests/unit_tests/core/ops/__init__.py b/api/tests/unit_tests/core/ops/__init__.py new file mode 100644 index 0000000000..bb92ccdec7 --- /dev/null +++ b/api/tests/unit_tests/core/ops/__init__.py @@ -0,0 +1 @@ +# Unit tests for core ops module diff --git a/api/tests/unit_tests/core/ops/test_config_entity.py b/api/tests/unit_tests/core/ops/test_config_entity.py new file mode 100644 index 0000000000..81cb04548d --- /dev/null +++ b/api/tests/unit_tests/core/ops/test_config_entity.py @@ -0,0 +1,385 @@ +import pytest +from pydantic import ValidationError + +from core.ops.entities.config_entity import ( + AliyunConfig, + ArizeConfig, + LangfuseConfig, + LangSmithConfig, + OpikConfig, + PhoenixConfig, + TracingProviderEnum, + WeaveConfig, +) + + +class TestTracingProviderEnum: + """Test cases for TracingProviderEnum""" + + def test_enum_values(self): + """Test that all expected enum values are present""" + assert TracingProviderEnum.ARIZE == "arize" + assert TracingProviderEnum.PHOENIX == "phoenix" + assert TracingProviderEnum.LANGFUSE == "langfuse" + assert TracingProviderEnum.LANGSMITH == "langsmith" + assert TracingProviderEnum.OPIK == "opik" + assert TracingProviderEnum.WEAVE == "weave" + assert TracingProviderEnum.ALIYUN == "aliyun" + + +class TestArizeConfig: + """Test cases for ArizeConfig""" + + def test_valid_config(self): + """Test valid Arize configuration""" + config = ArizeConfig( + api_key="test_key", space_id="test_space", project="test_project", endpoint="https://custom.arize.com" + ) + assert config.api_key == "test_key" + assert config.space_id == "test_space" + assert config.project == "test_project" + assert config.endpoint == "https://custom.arize.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = ArizeConfig() + assert config.api_key is None + assert config.space_id is None + assert config.project is None + assert config.endpoint == "https://otlp.arize.com" + + def test_project_validation_empty(self): + """Test project validation with empty value""" + config = ArizeConfig(project="") + assert config.project == "default" + + def test_project_validation_none(self): + """Test project validation with None value""" + config = ArizeConfig(project=None) + assert config.project == "default" + + def test_endpoint_validation_empty(self): + """Test endpoint validation with empty value""" + config = ArizeConfig(endpoint="") + assert config.endpoint == "https://otlp.arize.com" + + def test_endpoint_validation_with_path(self): + """Test endpoint validation normalizes URL by removing path""" + config = ArizeConfig(endpoint="https://custom.arize.com/api/v1") + assert config.endpoint == "https://custom.arize.com" + + def test_endpoint_validation_invalid_scheme(self): + """Test endpoint validation rejects invalid schemes""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + ArizeConfig(endpoint="ftp://invalid.com") + + def test_endpoint_validation_no_scheme(self): + """Test endpoint validation rejects URLs without scheme""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + ArizeConfig(endpoint="invalid.com") + + +class TestPhoenixConfig: + """Test cases for PhoenixConfig""" + + def test_valid_config(self): + """Test valid Phoenix configuration""" + config = PhoenixConfig(api_key="test_key", project="test_project", endpoint="https://custom.phoenix.com") + assert config.api_key == "test_key" + assert config.project == "test_project" + assert config.endpoint == "https://custom.phoenix.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = PhoenixConfig() + assert config.api_key is None + assert config.project is None + assert config.endpoint == "https://app.phoenix.arize.com" + + def test_project_validation_empty(self): + """Test project validation with empty value""" + config = PhoenixConfig(project="") + assert config.project == "default" + + def test_endpoint_validation_with_path(self): + """Test endpoint validation normalizes URL by removing path""" + config = PhoenixConfig(endpoint="https://custom.phoenix.com/api/v1") + assert config.endpoint == "https://custom.phoenix.com" + + +class TestLangfuseConfig: + """Test cases for LangfuseConfig""" + + def test_valid_config(self): + """Test valid Langfuse configuration""" + config = LangfuseConfig(public_key="public_key", secret_key="secret_key", host="https://custom.langfuse.com") + assert config.public_key == "public_key" + assert config.secret_key == "secret_key" + assert config.host == "https://custom.langfuse.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = LangfuseConfig(public_key="public", secret_key="secret") + assert config.host == "https://api.langfuse.com" + + def test_missing_required_fields(self): + """Test that required fields are enforced""" + with pytest.raises(ValidationError): + LangfuseConfig() + + with pytest.raises(ValidationError): + LangfuseConfig(public_key="public") + + with pytest.raises(ValidationError): + LangfuseConfig(secret_key="secret") + + def test_host_validation_empty(self): + """Test host validation with empty value""" + config = LangfuseConfig(public_key="public", secret_key="secret", host="") + assert config.host == "https://api.langfuse.com" + + +class TestLangSmithConfig: + """Test cases for LangSmithConfig""" + + def test_valid_config(self): + """Test valid LangSmith configuration""" + config = LangSmithConfig(api_key="test_key", project="test_project", endpoint="https://custom.smith.com") + assert config.api_key == "test_key" + assert config.project == "test_project" + assert config.endpoint == "https://custom.smith.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = LangSmithConfig(api_key="key", project="project") + assert config.endpoint == "https://api.smith.langchain.com" + + def test_missing_required_fields(self): + """Test that required fields are enforced""" + with pytest.raises(ValidationError): + LangSmithConfig() + + with pytest.raises(ValidationError): + LangSmithConfig(api_key="key") + + with pytest.raises(ValidationError): + LangSmithConfig(project="project") + + def test_endpoint_validation_https_only(self): + """Test endpoint validation only allows HTTPS""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + LangSmithConfig(api_key="key", project="project", endpoint="http://insecure.com") + + +class TestOpikConfig: + """Test cases for OpikConfig""" + + def test_valid_config(self): + """Test valid Opik configuration""" + config = OpikConfig( + api_key="test_key", + project="test_project", + workspace="test_workspace", + url="https://custom.comet.com/opik/api/", + ) + assert config.api_key == "test_key" + assert config.project == "test_project" + assert config.workspace == "test_workspace" + assert config.url == "https://custom.comet.com/opik/api/" + + def test_default_values(self): + """Test default values are set correctly""" + config = OpikConfig() + assert config.api_key is None + assert config.project is None + assert config.workspace is None + assert config.url == "https://www.comet.com/opik/api/" + + def test_project_validation_empty(self): + """Test project validation with empty value""" + config = OpikConfig(project="") + assert config.project == "Default Project" + + def test_url_validation_empty(self): + """Test URL validation with empty value""" + config = OpikConfig(url="") + assert config.url == "https://www.comet.com/opik/api/" + + def test_url_validation_missing_suffix(self): + """Test URL validation requires /api/ suffix""" + with pytest.raises(ValidationError, match="URL should end with /api/"): + OpikConfig(url="https://custom.comet.com/opik/") + + def test_url_validation_invalid_scheme(self): + """Test URL validation rejects invalid schemes""" + with pytest.raises(ValidationError, match="URL must start with https:// or http://"): + OpikConfig(url="ftp://custom.comet.com/opik/api/") + + +class TestWeaveConfig: + """Test cases for WeaveConfig""" + + def test_valid_config(self): + """Test valid Weave configuration""" + config = WeaveConfig( + api_key="test_key", + entity="test_entity", + project="test_project", + endpoint="https://custom.wandb.ai", + host="https://custom.host.com", + ) + assert config.api_key == "test_key" + assert config.entity == "test_entity" + assert config.project == "test_project" + assert config.endpoint == "https://custom.wandb.ai" + assert config.host == "https://custom.host.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = WeaveConfig(api_key="key", project="project") + assert config.entity is None + assert config.endpoint == "https://trace.wandb.ai" + assert config.host is None + + def test_missing_required_fields(self): + """Test that required fields are enforced""" + with pytest.raises(ValidationError): + WeaveConfig() + + with pytest.raises(ValidationError): + WeaveConfig(api_key="key") + + with pytest.raises(ValidationError): + WeaveConfig(project="project") + + def test_endpoint_validation_https_only(self): + """Test endpoint validation only allows HTTPS""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + WeaveConfig(api_key="key", project="project", endpoint="http://insecure.wandb.ai") + + def test_host_validation_optional(self): + """Test host validation is optional but validates when provided""" + config = WeaveConfig(api_key="key", project="project", host=None) + assert config.host is None + + config = WeaveConfig(api_key="key", project="project", host="") + assert config.host == "" + + config = WeaveConfig(api_key="key", project="project", host="https://valid.host.com") + assert config.host == "https://valid.host.com" + + def test_host_validation_invalid_scheme(self): + """Test host validation rejects invalid schemes when provided""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + WeaveConfig(api_key="key", project="project", host="ftp://invalid.host.com") + + +class TestAliyunConfig: + """Test cases for AliyunConfig""" + + def test_valid_config(self): + """Test valid Aliyun configuration""" + config = AliyunConfig( + app_name="test_app", + license_key="test_license_key", + endpoint="https://custom.tracing-analysis-dc-hz.aliyuncs.com", + ) + assert config.app_name == "test_app" + assert config.license_key == "test_license_key" + assert config.endpoint == "https://custom.tracing-analysis-dc-hz.aliyuncs.com" + + def test_default_values(self): + """Test default values are set correctly""" + config = AliyunConfig(license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com") + assert config.app_name == "dify_app" + + def test_missing_required_fields(self): + """Test that required fields are enforced""" + with pytest.raises(ValidationError): + AliyunConfig() + + with pytest.raises(ValidationError): + AliyunConfig(license_key="test_license") + + with pytest.raises(ValidationError): + AliyunConfig(endpoint="https://tracing-analysis-dc-hz.aliyuncs.com") + + def test_app_name_validation_empty(self): + """Test app_name validation with empty value""" + config = AliyunConfig( + license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com", app_name="" + ) + assert config.app_name == "dify_app" + + def test_endpoint_validation_empty(self): + """Test endpoint validation with empty value""" + config = AliyunConfig(license_key="test_license", endpoint="") + assert config.endpoint == "https://tracing-analysis-dc-hz.aliyuncs.com" + + def test_endpoint_validation_with_path(self): + """Test endpoint validation normalizes URL by removing path""" + config = AliyunConfig( + license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com/api/v1/traces" + ) + assert config.endpoint == "https://tracing-analysis-dc-hz.aliyuncs.com" + + def test_endpoint_validation_invalid_scheme(self): + """Test endpoint validation rejects invalid schemes""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + AliyunConfig(license_key="test_license", endpoint="ftp://invalid.tracing-analysis-dc-hz.aliyuncs.com") + + def test_endpoint_validation_no_scheme(self): + """Test endpoint validation rejects URLs without scheme""" + with pytest.raises(ValidationError, match="URL scheme must be one of"): + AliyunConfig(license_key="test_license", endpoint="invalid.tracing-analysis-dc-hz.aliyuncs.com") + + def test_license_key_required(self): + """Test that license_key is required and cannot be empty""" + with pytest.raises(ValidationError): + AliyunConfig(license_key="", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com") + + +class TestConfigIntegration: + """Integration tests for configuration classes""" + + def test_all_configs_can_be_instantiated(self): + """Test that all config classes can be instantiated with valid data""" + configs = [ + ArizeConfig(api_key="key"), + PhoenixConfig(api_key="key"), + LangfuseConfig(public_key="public", secret_key="secret"), + LangSmithConfig(api_key="key", project="project"), + OpikConfig(api_key="key"), + WeaveConfig(api_key="key", project="project"), + AliyunConfig(license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com"), + ] + + for config in configs: + assert config is not None + + def test_url_normalization_consistency(self): + """Test that URL normalization works consistently across configs""" + # Test that paths are removed from endpoints + arize_config = ArizeConfig(endpoint="https://arize.com/api/v1/test") + phoenix_config = PhoenixConfig(endpoint="https://phoenix.com/api/v2/") + aliyun_config = AliyunConfig( + license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com/api/v1/traces" + ) + + assert arize_config.endpoint == "https://arize.com" + assert phoenix_config.endpoint == "https://phoenix.com" + assert aliyun_config.endpoint == "https://tracing-analysis-dc-hz.aliyuncs.com" + + def test_project_default_values(self): + """Test that project default values are set correctly""" + arize_config = ArizeConfig(project="") + phoenix_config = PhoenixConfig(project="") + opik_config = OpikConfig(project="") + aliyun_config = AliyunConfig( + license_key="test_license", endpoint="https://tracing-analysis-dc-hz.aliyuncs.com", app_name="" + ) + + assert arize_config.project == "default" + assert phoenix_config.project == "default" + assert opik_config.project == "Default Project" + assert aliyun_config.app_name == "dify_app" diff --git a/api/tests/unit_tests/core/ops/test_utils.py b/api/tests/unit_tests/core/ops/test_utils.py new file mode 100644 index 0000000000..7cc2772acf --- /dev/null +++ b/api/tests/unit_tests/core/ops/test_utils.py @@ -0,0 +1,138 @@ +import pytest + +from core.ops.utils import validate_project_name, validate_url, validate_url_with_path + + +class TestValidateUrl: + """Test cases for validate_url function""" + + def test_valid_https_url(self): + """Test valid HTTPS URL""" + result = validate_url("https://example.com", "https://default.com") + assert result == "https://example.com" + + def test_valid_http_url(self): + """Test valid HTTP URL""" + result = validate_url("http://example.com", "https://default.com") + assert result == "http://example.com" + + def test_url_with_path_removed(self): + """Test that URL path is removed during normalization""" + result = validate_url("https://example.com/api/v1/test", "https://default.com") + assert result == "https://example.com" + + def test_url_with_query_removed(self): + """Test that URL query parameters are removed""" + result = validate_url("https://example.com?param=value", "https://default.com") + assert result == "https://example.com" + + def test_url_with_fragment_removed(self): + """Test that URL fragments are removed""" + result = validate_url("https://example.com#section", "https://default.com") + assert result == "https://example.com" + + def test_empty_url_returns_default(self): + """Test empty URL returns default""" + result = validate_url("", "https://default.com") + assert result == "https://default.com" + + def test_none_url_returns_default(self): + """Test None URL returns default""" + result = validate_url(None, "https://default.com") + assert result == "https://default.com" + + def test_whitespace_url_returns_default(self): + """Test whitespace URL returns default""" + result = validate_url(" ", "https://default.com") + assert result == "https://default.com" + + def test_invalid_scheme_raises_error(self): + """Test invalid scheme raises ValueError""" + with pytest.raises(ValueError, match="URL scheme must be one of"): + validate_url("ftp://example.com", "https://default.com") + + def test_no_scheme_raises_error(self): + """Test URL without scheme raises ValueError""" + with pytest.raises(ValueError, match="URL scheme must be one of"): + validate_url("example.com", "https://default.com") + + def test_custom_allowed_schemes(self): + """Test custom allowed schemes""" + result = validate_url("https://example.com", "https://default.com", allowed_schemes=("https",)) + assert result == "https://example.com" + + with pytest.raises(ValueError, match="URL scheme must be one of"): + validate_url("http://example.com", "https://default.com", allowed_schemes=("https",)) + + +class TestValidateUrlWithPath: + """Test cases for validate_url_with_path function""" + + def test_valid_url_with_path(self): + """Test valid URL with path""" + result = validate_url_with_path("https://example.com/api/v1", "https://default.com") + assert result == "https://example.com/api/v1" + + def test_valid_url_with_required_suffix(self): + """Test valid URL with required suffix""" + result = validate_url_with_path("https://example.com/api/", "https://default.com", required_suffix="/api/") + assert result == "https://example.com/api/" + + def test_url_without_required_suffix_raises_error(self): + """Test URL without required suffix raises error""" + with pytest.raises(ValueError, match="URL should end with /api/"): + validate_url_with_path("https://example.com/api", "https://default.com", required_suffix="/api/") + + def test_empty_url_returns_default(self): + """Test empty URL returns default""" + result = validate_url_with_path("", "https://default.com") + assert result == "https://default.com" + + def test_none_url_returns_default(self): + """Test None URL returns default""" + result = validate_url_with_path(None, "https://default.com") + assert result == "https://default.com" + + def test_invalid_scheme_raises_error(self): + """Test invalid scheme raises ValueError""" + with pytest.raises(ValueError, match="URL must start with https:// or http://"): + validate_url_with_path("ftp://example.com", "https://default.com") + + def test_no_scheme_raises_error(self): + """Test URL without scheme raises ValueError""" + with pytest.raises(ValueError, match="URL must start with https:// or http://"): + validate_url_with_path("example.com", "https://default.com") + + +class TestValidateProjectName: + """Test cases for validate_project_name function""" + + def test_valid_project_name(self): + """Test valid project name""" + result = validate_project_name("my-project", "default") + assert result == "my-project" + + def test_empty_project_name_returns_default(self): + """Test empty project name returns default""" + result = validate_project_name("", "default") + assert result == "default" + + def test_none_project_name_returns_default(self): + """Test None project name returns default""" + result = validate_project_name(None, "default") + assert result == "default" + + def test_whitespace_project_name_returns_default(self): + """Test whitespace project name returns default""" + result = validate_project_name(" ", "default") + assert result == "default" + + def test_project_name_with_whitespace_trimmed(self): + """Test project name with whitespace is trimmed""" + result = validate_project_name(" my-project ", "default") + assert result == "my-project" + + def test_custom_default_name(self): + """Test custom default name""" + result = validate_project_name("", "Custom Default") + assert result == "Custom Default" diff --git a/api/tests/unit_tests/core/rag/extractor/test_markdown_extractor.py b/api/tests/unit_tests/core/rag/extractor/test_markdown_extractor.py new file mode 100644 index 0000000000..d4cf534c56 --- /dev/null +++ b/api/tests/unit_tests/core/rag/extractor/test_markdown_extractor.py @@ -0,0 +1,22 @@ +from core.rag.extractor.markdown_extractor import MarkdownExtractor + + +def test_markdown_to_tups(): + markdown = """ +this is some text without header + +# title 1 +this is balabala text + +## title 2 +this is more specific text. + """ + extractor = MarkdownExtractor(file_path="dummy_path") + updated_output = extractor.markdown_to_tups(markdown) + assert len(updated_output) == 3 + key, header_value = updated_output[0] + assert key == None + assert header_value.strip() == "this is some text without header" + title_1, value = updated_output[1] + assert title_1.strip() == "title 1" + assert value.strip() == "this is balabala text" diff --git a/api/tests/unit_tests/core/repositories/__init__.py b/api/tests/unit_tests/core/repositories/__init__.py new file mode 100644 index 0000000000..c65d7da61d --- /dev/null +++ b/api/tests/unit_tests/core/repositories/__init__.py @@ -0,0 +1 @@ +# Unit tests for core repositories module diff --git a/api/tests/unit_tests/core/repositories/test_factory.py b/api/tests/unit_tests/core/repositories/test_factory.py new file mode 100644 index 0000000000..fce4a6fb6b --- /dev/null +++ b/api/tests/unit_tests/core/repositories/test_factory.py @@ -0,0 +1,455 @@ +""" +Unit tests for the RepositoryFactory. + +This module tests the factory pattern implementation for creating repository instances +based on configuration, including error handling and validation. +""" + +from unittest.mock import MagicMock, patch + +import pytest +from pytest_mock import MockerFixture +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker + +from core.repositories.factory import DifyCoreRepositoryFactory, RepositoryImportError +from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository +from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from models import Account, EndUser +from models.enums import WorkflowRunTriggeredFrom +from models.workflow import WorkflowNodeExecutionTriggeredFrom + + +class TestRepositoryFactory: + """Test cases for RepositoryFactory.""" + + def test_import_class_success(self): + """Test successful class import.""" + # Test importing a real class + class_path = "unittest.mock.MagicMock" + result = DifyCoreRepositoryFactory._import_class(class_path) + assert result is MagicMock + + def test_import_class_invalid_path(self): + """Test import with invalid module path.""" + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._import_class("invalid.module.path") + assert "Cannot import repository class" in str(exc_info.value) + + def test_import_class_invalid_class_name(self): + """Test import with invalid class name.""" + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._import_class("unittest.mock.NonExistentClass") + assert "Cannot import repository class" in str(exc_info.value) + + def test_import_class_malformed_path(self): + """Test import with malformed path (no dots).""" + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._import_class("invalidpath") + assert "Cannot import repository class" in str(exc_info.value) + + def test_validate_repository_interface_success(self): + """Test successful interface validation.""" + + # Create a mock class that implements the required methods + class MockRepository: + def save(self): + pass + + def get_by_id(self): + pass + + # Create a mock interface with the same methods + class MockInterface: + def save(self): + pass + + def get_by_id(self): + pass + + # Should not raise an exception + DifyCoreRepositoryFactory._validate_repository_interface(MockRepository, MockInterface) + + def test_validate_repository_interface_missing_methods(self): + """Test interface validation with missing methods.""" + + # Create a mock class that doesn't implement all required methods + class IncompleteRepository: + def save(self): + pass + + # Missing get_by_id method + + # Create a mock interface with required methods + class MockInterface: + def save(self): + pass + + def get_by_id(self): + pass + + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._validate_repository_interface(IncompleteRepository, MockInterface) + assert "does not implement required methods" in str(exc_info.value) + assert "get_by_id" in str(exc_info.value) + + def test_validate_constructor_signature_success(self): + """Test successful constructor signature validation.""" + + class MockRepository: + def __init__(self, session_factory, user, app_id, triggered_from): + pass + + # Should not raise an exception + DifyCoreRepositoryFactory._validate_constructor_signature( + MockRepository, ["session_factory", "user", "app_id", "triggered_from"] + ) + + def test_validate_constructor_signature_missing_params(self): + """Test constructor validation with missing parameters.""" + + class IncompleteRepository: + def __init__(self, session_factory, user): + # Missing app_id and triggered_from parameters + pass + + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._validate_constructor_signature( + IncompleteRepository, ["session_factory", "user", "app_id", "triggered_from"] + ) + assert "does not accept required parameters" in str(exc_info.value) + assert "app_id" in str(exc_info.value) + assert "triggered_from" in str(exc_info.value) + + def test_validate_constructor_signature_inspection_error(self, mocker: MockerFixture): + """Test constructor validation when inspection fails.""" + # Mock inspect.signature to raise an exception + mocker.patch("inspect.signature", side_effect=Exception("Inspection failed")) + + class MockRepository: + def __init__(self, session_factory): + pass + + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._validate_constructor_signature(MockRepository, ["session_factory"]) + assert "Failed to validate constructor signature" in str(exc_info.value) + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_execution_repository_success(self, mock_config, mocker: MockerFixture): + """Test successful creation of WorkflowExecutionRepository.""" + # Setup mock configuration + mock_config.WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + # Create mock dependencies + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=Account) + app_id = "test-app-id" + triggered_from = WorkflowRunTriggeredFrom.APP_RUN + + # Mock the imported class to be a valid repository + mock_repository_class = MagicMock() + mock_repository_instance = MagicMock(spec=WorkflowExecutionRepository) + mock_repository_class.return_value = mock_repository_instance + + # Mock the validation methods + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object(DifyCoreRepositoryFactory, "_validate_repository_interface"), + patch.object(DifyCoreRepositoryFactory, "_validate_constructor_signature"), + ): + result = DifyCoreRepositoryFactory.create_workflow_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id=app_id, + triggered_from=triggered_from, + ) + + # Verify the repository was created with correct parameters + mock_repository_class.assert_called_once_with( + session_factory=mock_session_factory, + user=mock_user, + app_id=app_id, + triggered_from=triggered_from, + ) + assert result is mock_repository_instance + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_execution_repository_import_error(self, mock_config): + """Test WorkflowExecutionRepository creation with import error.""" + # Setup mock configuration with invalid class path + mock_config.WORKFLOW_EXECUTION_REPOSITORY = "invalid.module.InvalidClass" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=Account) + + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + assert "Cannot import repository class" in str(exc_info.value) + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_execution_repository_validation_error(self, mock_config, mocker: MockerFixture): + """Test WorkflowExecutionRepository creation with validation error.""" + # Setup mock configuration + mock_config.WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=Account) + + # Mock import to succeed but validation to fail + mock_repository_class = MagicMock() + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object( + DifyCoreRepositoryFactory, + "_validate_repository_interface", + side_effect=RepositoryImportError("Interface validation failed"), + ), + ): + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + assert "Interface validation failed" in str(exc_info.value) + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_execution_repository_instantiation_error(self, mock_config, mocker: MockerFixture): + """Test WorkflowExecutionRepository creation with instantiation error.""" + # Setup mock configuration + mock_config.WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=Account) + + # Mock import and validation to succeed but instantiation to fail + mock_repository_class = MagicMock(side_effect=Exception("Instantiation failed")) + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object(DifyCoreRepositoryFactory, "_validate_repository_interface"), + patch.object(DifyCoreRepositoryFactory, "_validate_constructor_signature"), + ): + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + assert "Failed to create WorkflowExecutionRepository" in str(exc_info.value) + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_node_execution_repository_success(self, mock_config, mocker: MockerFixture): + """Test successful creation of WorkflowNodeExecutionRepository.""" + # Setup mock configuration + mock_config.WORKFLOW_NODE_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + # Create mock dependencies + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=EndUser) + app_id = "test-app-id" + triggered_from = WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN + + # Mock the imported class to be a valid repository + mock_repository_class = MagicMock() + mock_repository_instance = MagicMock(spec=WorkflowNodeExecutionRepository) + mock_repository_class.return_value = mock_repository_instance + + # Mock the validation methods + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object(DifyCoreRepositoryFactory, "_validate_repository_interface"), + patch.object(DifyCoreRepositoryFactory, "_validate_constructor_signature"), + ): + result = DifyCoreRepositoryFactory.create_workflow_node_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id=app_id, + triggered_from=triggered_from, + ) + + # Verify the repository was created with correct parameters + mock_repository_class.assert_called_once_with( + session_factory=mock_session_factory, + user=mock_user, + app_id=app_id, + triggered_from=triggered_from, + ) + assert result is mock_repository_instance + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_node_execution_repository_import_error(self, mock_config): + """Test WorkflowNodeExecutionRepository creation with import error.""" + # Setup mock configuration with invalid class path + mock_config.WORKFLOW_NODE_EXECUTION_REPOSITORY = "invalid.module.InvalidClass" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=EndUser) + + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_node_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, + ) + assert "Cannot import repository class" in str(exc_info.value) + + def test_repository_import_error_exception(self): + """Test RepositoryImportError exception.""" + error_message = "Test error message" + exception = RepositoryImportError(error_message) + assert str(exception) == error_message + assert isinstance(exception, Exception) + + @patch("core.repositories.factory.dify_config") + def test_create_with_engine_instead_of_sessionmaker(self, mock_config, mocker: MockerFixture): + """Test repository creation with Engine instead of sessionmaker.""" + # Setup mock configuration + mock_config.WORKFLOW_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + # Create mock dependencies with Engine instead of sessionmaker + mock_engine = MagicMock(spec=Engine) + mock_user = MagicMock(spec=Account) + + # Mock the imported class to be a valid repository + mock_repository_class = MagicMock() + mock_repository_instance = MagicMock(spec=WorkflowExecutionRepository) + mock_repository_class.return_value = mock_repository_instance + + # Mock the validation methods + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object(DifyCoreRepositoryFactory, "_validate_repository_interface"), + patch.object(DifyCoreRepositoryFactory, "_validate_constructor_signature"), + ): + result = DifyCoreRepositoryFactory.create_workflow_execution_repository( + session_factory=mock_engine, # Using Engine instead of sessionmaker + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + + # Verify the repository was created with the Engine + mock_repository_class.assert_called_once_with( + session_factory=mock_engine, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + ) + assert result is mock_repository_instance + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_node_execution_repository_validation_error(self, mock_config): + """Test WorkflowNodeExecutionRepository creation with validation error.""" + # Setup mock configuration + mock_config.WORKFLOW_NODE_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=EndUser) + + # Mock import to succeed but validation to fail + mock_repository_class = MagicMock() + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object( + DifyCoreRepositoryFactory, + "_validate_repository_interface", + side_effect=RepositoryImportError("Interface validation failed"), + ), + ): + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_node_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, + ) + assert "Interface validation failed" in str(exc_info.value) + + @patch("core.repositories.factory.dify_config") + def test_create_workflow_node_execution_repository_instantiation_error(self, mock_config): + """Test WorkflowNodeExecutionRepository creation with instantiation error.""" + # Setup mock configuration + mock_config.WORKFLOW_NODE_EXECUTION_REPOSITORY = "unittest.mock.MagicMock" + + mock_session_factory = MagicMock(spec=sessionmaker) + mock_user = MagicMock(spec=EndUser) + + # Mock import and validation to succeed but instantiation to fail + mock_repository_class = MagicMock(side_effect=Exception("Instantiation failed")) + with ( + patch.object(DifyCoreRepositoryFactory, "_import_class", return_value=mock_repository_class), + patch.object(DifyCoreRepositoryFactory, "_validate_repository_interface"), + patch.object(DifyCoreRepositoryFactory, "_validate_constructor_signature"), + ): + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory.create_workflow_node_execution_repository( + session_factory=mock_session_factory, + user=mock_user, + app_id="test-app-id", + triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN, + ) + assert "Failed to create WorkflowNodeExecutionRepository" in str(exc_info.value) + + def test_validate_repository_interface_with_private_methods(self): + """Test interface validation ignores private methods.""" + + # Create a mock class with private methods + class MockRepository: + def save(self): + pass + + def get_by_id(self): + pass + + def _private_method(self): + pass + + # Create a mock interface with private methods + class MockInterface: + def save(self): + pass + + def get_by_id(self): + pass + + def _private_method(self): + pass + + # Should not raise an exception (private methods are ignored) + DifyCoreRepositoryFactory._validate_repository_interface(MockRepository, MockInterface) + + def test_validate_constructor_signature_with_extra_params(self): + """Test constructor validation with extra parameters (should pass).""" + + class MockRepository: + def __init__(self, session_factory, user, app_id, triggered_from, extra_param=None): + pass + + # Should not raise an exception (extra parameters are allowed) + DifyCoreRepositoryFactory._validate_constructor_signature( + MockRepository, ["session_factory", "user", "app_id", "triggered_from"] + ) + + def test_validate_constructor_signature_with_kwargs(self): + """Test constructor validation with **kwargs (current implementation doesn't support this).""" + + class MockRepository: + def __init__(self, session_factory, user, **kwargs): + pass + + # Current implementation doesn't handle **kwargs, so this should raise an exception + with pytest.raises(RepositoryImportError) as exc_info: + DifyCoreRepositoryFactory._validate_constructor_signature( + MockRepository, ["session_factory", "user", "app_id", "triggered_from"] + ) + assert "does not accept required parameters" in str(exc_info.value) + assert "app_id" in str(exc_info.value) + assert "triggered_from" in str(exc_info.value) diff --git a/api/tests/unit_tests/core/tools/utils/__init__.py b/api/tests/unit_tests/core/tools/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/core/tools/utils/test_parser.py b/api/tests/unit_tests/core/tools/utils/test_parser.py new file mode 100644 index 0000000000..8e07293ce0 --- /dev/null +++ b/api/tests/unit_tests/core/tools/utils/test_parser.py @@ -0,0 +1,56 @@ +import pytest +from flask import Flask + +from core.tools.utils.parser import ApiBasedToolSchemaParser + + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +def test_parse_openapi_to_tool_bundle_operation_id(app): + openapi = { + "openapi": "3.0.0", + "info": {"title": "Simple API", "version": "1.0.0"}, + "servers": [{"url": "http://localhost:3000"}], + "paths": { + "/": { + "get": { + "summary": "Root endpoint", + "responses": { + "200": { + "description": "Successful response", + } + }, + } + }, + "/api/resources": { + "get": { + "summary": "Non-root endpoint without an operationId", + "responses": { + "200": { + "description": "Successful response", + } + }, + }, + "post": { + "summary": "Non-root endpoint with an operationId", + "operationId": "createResource", + "responses": { + "201": { + "description": "Resource created", + } + }, + }, + }, + }, + } + with app.test_request_context(): + tool_bundles = ApiBasedToolSchemaParser.parse_openapi_to_tool_bundle(openapi) + + assert len(tool_bundles) == 3 + assert tool_bundles[0].operation_id == "_get" + assert tool_bundles[1].operation_id == "apiresources_get" + assert tool_bundles[2].operation_id == "createResource" diff --git a/api/tests/unit_tests/core/variables/test_segment.py b/api/tests/unit_tests/core/variables/test_segment.py new file mode 100644 index 0000000000..cdc261fd42 --- /dev/null +++ b/api/tests/unit_tests/core/variables/test_segment.py @@ -0,0 +1,385 @@ +import dataclasses + +from pydantic import BaseModel + +from core.file import File, FileTransferMethod, FileType +from core.helper import encrypter +from core.variables.segments import ( + ArrayAnySegment, + ArrayFileSegment, + ArrayNumberSegment, + ArrayObjectSegment, + ArrayStringSegment, + FileSegment, + FloatSegment, + IntegerSegment, + NoneSegment, + ObjectSegment, + Segment, + SegmentUnion, + StringSegment, + get_segment_discriminator, +) +from core.variables.types import SegmentType +from core.variables.variables import ( + ArrayAnyVariable, + ArrayFileVariable, + ArrayNumberVariable, + ArrayObjectVariable, + ArrayStringVariable, + FileVariable, + FloatVariable, + IntegerVariable, + NoneVariable, + ObjectVariable, + SecretVariable, + StringVariable, + Variable, + VariableUnion, +) +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.system_variable import SystemVariable + + +def test_segment_group_to_text(): + variable_pool = VariablePool( + system_variables=SystemVariable(user_id="fake-user-id"), + user_inputs={}, + environment_variables=[ + SecretVariable(name="secret_key", value="fake-secret-key"), + ], + conversation_variables=[], + ) + variable_pool.add(("node_id", "custom_query"), "fake-user-query") + template = ( + "Hello, {{#sys.user_id#}}! Your query is {{#node_id.custom_query#}}. And your key is {{#env.secret_key#}}." + ) + segments_group = variable_pool.convert_template(template) + + assert segments_group.text == "Hello, fake-user-id! Your query is fake-user-query. And your key is fake-secret-key." + assert segments_group.log == ( + f"Hello, fake-user-id! Your query is fake-user-query." + f" And your key is {encrypter.obfuscated_token('fake-secret-key')}." + ) + + +def test_convert_constant_to_segment_group(): + variable_pool = VariablePool( + system_variables=SystemVariable(user_id="1", app_id="1", workflow_id="1"), + user_inputs={}, + environment_variables=[], + conversation_variables=[], + ) + template = "Hello, world!" + segments_group = variable_pool.convert_template(template) + assert segments_group.text == "Hello, world!" + assert segments_group.log == "Hello, world!" + + +def test_convert_variable_to_segment_group(): + variable_pool = VariablePool( + system_variables=SystemVariable(user_id="fake-user-id"), + user_inputs={}, + environment_variables=[], + conversation_variables=[], + ) + template = "{{#sys.user_id#}}" + segments_group = variable_pool.convert_template(template) + assert segments_group.text == "fake-user-id" + assert segments_group.log == "fake-user-id" + assert isinstance(segments_group.value[0], StringVariable) + assert segments_group.value[0].value == "fake-user-id" + + +class _Segments(BaseModel): + segments: list[SegmentUnion] + + +class _Variables(BaseModel): + variables: list[VariableUnion] + + +def create_test_file( + file_type: FileType = FileType.DOCUMENT, + transfer_method: FileTransferMethod = FileTransferMethod.LOCAL_FILE, + filename: str = "test.txt", + extension: str = ".txt", + mime_type: str = "text/plain", + size: int = 1024, +) -> File: + """Factory function to create File objects for testing""" + return File( + tenant_id="test-tenant", + type=file_type, + transfer_method=transfer_method, + filename=filename, + extension=extension, + mime_type=mime_type, + size=size, + related_id="test-file-id" if transfer_method != FileTransferMethod.REMOTE_URL else None, + remote_url="https://example.com/file.txt" if transfer_method == FileTransferMethod.REMOTE_URL else None, + storage_key="test-storage-key", + ) + + +class TestSegmentDumpAndLoad: + """Test suite for segment and variable serialization/deserialization""" + + def test_segments(self): + """Test basic segment serialization compatibility""" + model = _Segments(segments=[IntegerSegment(value=1), StringSegment(value="a")]) + json = model.model_dump_json() + print("Json: ", json) + loaded = _Segments.model_validate_json(json) + assert loaded == model + + def test_segment_number(self): + """Test number segment serialization compatibility""" + model = _Segments(segments=[IntegerSegment(value=1), FloatSegment(value=1.0)]) + json = model.model_dump_json() + print("Json: ", json) + loaded = _Segments.model_validate_json(json) + assert loaded == model + + def test_variables(self): + """Test variable serialization compatibility""" + model = _Variables(variables=[IntegerVariable(value=1, name="int"), StringVariable(value="a", name="str")]) + json = model.model_dump_json() + print("Json: ", json) + restored = _Variables.model_validate_json(json) + assert restored == model + + def test_all_segments_serialization(self): + """Test serialization/deserialization of all segment types""" + # Create one instance of each segment type + test_file = create_test_file() + + all_segments: list[SegmentUnion] = [ + NoneSegment(), + StringSegment(value="test string"), + IntegerSegment(value=42), + FloatSegment(value=3.14), + ObjectSegment(value={"key": "value", "number": 123}), + FileSegment(value=test_file), + ArrayAnySegment(value=[1, "string", 3.14, {"key": "value"}]), + ArrayStringSegment(value=["hello", "world"]), + ArrayNumberSegment(value=[1, 2.5, 3]), + ArrayObjectSegment(value=[{"id": 1}, {"id": 2}]), + ArrayFileSegment(value=[]), # Empty array to avoid file complexity + ] + + # Test serialization and deserialization + model = _Segments(segments=all_segments) + json_str = model.model_dump_json() + loaded = _Segments.model_validate_json(json_str) + + # Verify all segments are preserved + assert len(loaded.segments) == len(all_segments) + + for original, loaded_segment in zip(all_segments, loaded.segments): + assert type(loaded_segment) == type(original) + assert loaded_segment.value_type == original.value_type + + # For file segments, compare key properties instead of exact equality + if isinstance(original, FileSegment) and isinstance(loaded_segment, FileSegment): + orig_file = original.value + loaded_file = loaded_segment.value + assert isinstance(orig_file, File) + assert isinstance(loaded_file, File) + assert loaded_file.tenant_id == orig_file.tenant_id + assert loaded_file.type == orig_file.type + assert loaded_file.filename == orig_file.filename + else: + assert loaded_segment.value == original.value + + def test_all_variables_serialization(self): + """Test serialization/deserialization of all variable types""" + # Create one instance of each variable type + test_file = create_test_file() + + all_variables: list[VariableUnion] = [ + NoneVariable(name="none_var"), + StringVariable(value="test string", name="string_var"), + IntegerVariable(value=42, name="int_var"), + FloatVariable(value=3.14, name="float_var"), + ObjectVariable(value={"key": "value", "number": 123}, name="object_var"), + FileVariable(value=test_file, name="file_var"), + ArrayAnyVariable(value=[1, "string", 3.14, {"key": "value"}], name="array_any_var"), + ArrayStringVariable(value=["hello", "world"], name="array_string_var"), + ArrayNumberVariable(value=[1, 2.5, 3], name="array_number_var"), + ArrayObjectVariable(value=[{"id": 1}, {"id": 2}], name="array_object_var"), + ArrayFileVariable(value=[], name="array_file_var"), # Empty array to avoid file complexity + ] + + # Test serialization and deserialization + model = _Variables(variables=all_variables) + json_str = model.model_dump_json() + loaded = _Variables.model_validate_json(json_str) + + # Verify all variables are preserved + assert len(loaded.variables) == len(all_variables) + + for original, loaded_variable in zip(all_variables, loaded.variables): + assert type(loaded_variable) == type(original) + assert loaded_variable.value_type == original.value_type + assert loaded_variable.name == original.name + + # For file variables, compare key properties instead of exact equality + if isinstance(original, FileVariable) and isinstance(loaded_variable, FileVariable): + orig_file = original.value + loaded_file = loaded_variable.value + assert isinstance(orig_file, File) + assert isinstance(loaded_file, File) + assert loaded_file.tenant_id == orig_file.tenant_id + assert loaded_file.type == orig_file.type + assert loaded_file.filename == orig_file.filename + else: + assert loaded_variable.value == original.value + + def test_segment_discriminator_function_for_segment_types(self): + """Test the segment discriminator function""" + + @dataclasses.dataclass + class TestCase: + segment: Segment + expected_segment_type: SegmentType + + file1 = create_test_file() + file2 = create_test_file(filename="test2.txt") + + cases = [ + TestCase( + NoneSegment(), + SegmentType.NONE, + ), + TestCase( + StringSegment(value=""), + SegmentType.STRING, + ), + TestCase( + FloatSegment(value=0.0), + SegmentType.FLOAT, + ), + TestCase( + IntegerSegment(value=0), + SegmentType.INTEGER, + ), + TestCase( + ObjectSegment(value={}), + SegmentType.OBJECT, + ), + TestCase( + FileSegment(value=file1), + SegmentType.FILE, + ), + TestCase( + ArrayAnySegment(value=[0, 0.0, ""]), + SegmentType.ARRAY_ANY, + ), + TestCase( + ArrayStringSegment(value=[""]), + SegmentType.ARRAY_STRING, + ), + TestCase( + ArrayNumberSegment(value=[0, 0.0]), + SegmentType.ARRAY_NUMBER, + ), + TestCase( + ArrayObjectSegment(value=[{}]), + SegmentType.ARRAY_OBJECT, + ), + TestCase( + ArrayFileSegment(value=[file1, file2]), + SegmentType.ARRAY_FILE, + ), + ] + + for test_case in cases: + segment = test_case.segment + assert get_segment_discriminator(segment) == test_case.expected_segment_type, ( + f"get_segment_discriminator failed for type {type(segment)}" + ) + model_dict = segment.model_dump(mode="json") + assert get_segment_discriminator(model_dict) == test_case.expected_segment_type, ( + f"get_segment_discriminator failed for serialized form of type {type(segment)}" + ) + + def test_variable_discriminator_function_for_variable_types(self): + """Test the variable discriminator function""" + + @dataclasses.dataclass + class TestCase: + variable: Variable + expected_segment_type: SegmentType + + file1 = create_test_file() + file2 = create_test_file(filename="test2.txt") + + cases = [ + TestCase( + NoneVariable(name="none_var"), + SegmentType.NONE, + ), + TestCase( + StringVariable(value="test", name="string_var"), + SegmentType.STRING, + ), + TestCase( + FloatVariable(value=0.0, name="float_var"), + SegmentType.FLOAT, + ), + TestCase( + IntegerVariable(value=0, name="int_var"), + SegmentType.INTEGER, + ), + TestCase( + ObjectVariable(value={}, name="object_var"), + SegmentType.OBJECT, + ), + TestCase( + FileVariable(value=file1, name="file_var"), + SegmentType.FILE, + ), + TestCase( + SecretVariable(value="secret", name="secret_var"), + SegmentType.SECRET, + ), + TestCase( + ArrayAnyVariable(value=[0, 0.0, ""], name="array_any_var"), + SegmentType.ARRAY_ANY, + ), + TestCase( + ArrayStringVariable(value=[""], name="array_string_var"), + SegmentType.ARRAY_STRING, + ), + TestCase( + ArrayNumberVariable(value=[0, 0.0], name="array_number_var"), + SegmentType.ARRAY_NUMBER, + ), + TestCase( + ArrayObjectVariable(value=[{}], name="array_object_var"), + SegmentType.ARRAY_OBJECT, + ), + TestCase( + ArrayFileVariable(value=[file1, file2], name="array_file_var"), + SegmentType.ARRAY_FILE, + ), + ] + + for test_case in cases: + variable = test_case.variable + assert get_segment_discriminator(variable) == test_case.expected_segment_type, ( + f"get_segment_discriminator failed for type {type(variable)}" + ) + model_dict = variable.model_dump(mode="json") + assert get_segment_discriminator(model_dict) == test_case.expected_segment_type, ( + f"get_segment_discriminator failed for serialized form of type {type(variable)}" + ) + + def test_invlaid_value_for_discriminator(self): + # Test invalid cases + assert get_segment_discriminator({"value_type": "invalid"}) is None + assert get_segment_discriminator({}) is None + assert get_segment_discriminator("not_a_dict") is None + assert get_segment_discriminator(42) is None + assert get_segment_discriminator(object) is None diff --git a/api/tests/unit_tests/core/variables/test_segment_type.py b/api/tests/unit_tests/core/variables/test_segment_type.py new file mode 100644 index 0000000000..64d0d8c7e7 --- /dev/null +++ b/api/tests/unit_tests/core/variables/test_segment_type.py @@ -0,0 +1,60 @@ +from core.variables.types import SegmentType + + +class TestSegmentTypeIsArrayType: + """ + Test class for SegmentType.is_array_type method. + + Provides comprehensive coverage of all SegmentType values to ensure + correct identification of array and non-array types. + """ + + def test_is_array_type(self): + """ + Test that all SegmentType enum values are covered in our test cases. + + Ensures comprehensive coverage by verifying that every SegmentType + value is tested for the is_array_type method. + """ + # Arrange + all_segment_types = set(SegmentType) + expected_array_types = [ + SegmentType.ARRAY_ANY, + SegmentType.ARRAY_STRING, + SegmentType.ARRAY_NUMBER, + SegmentType.ARRAY_OBJECT, + SegmentType.ARRAY_FILE, + ] + expected_non_array_types = [ + SegmentType.INTEGER, + SegmentType.FLOAT, + SegmentType.NUMBER, + SegmentType.STRING, + SegmentType.OBJECT, + SegmentType.SECRET, + SegmentType.FILE, + SegmentType.NONE, + SegmentType.GROUP, + ] + + for seg_type in expected_array_types: + assert seg_type.is_array_type() + + for seg_type in expected_non_array_types: + assert not seg_type.is_array_type() + + # Act & Assert + covered_types = set(expected_array_types) | set(expected_non_array_types) + assert covered_types == set(SegmentType), "All SegmentType values should be covered in tests" + + def test_all_enum_values_are_supported(self): + """ + Test that all enum values are supported and return boolean values. + + Validates that every SegmentType enum value can be processed by + is_array_type method and returns a boolean value. + """ + enum_values: list[SegmentType] = list(SegmentType) + for seg_type in enum_values: + is_array = seg_type.is_array_type() + assert isinstance(is_array, bool), f"is_array_type does not return a boolean for segment type {seg_type}" diff --git a/api/tests/unit_tests/core/app/segments/test_variables.py b/api/tests/unit_tests/core/variables/test_variables.py similarity index 95% rename from api/tests/unit_tests/core/app/segments/test_variables.py rename to api/tests/unit_tests/core/variables/test_variables.py index 426557c716..925142892c 100644 --- a/api/tests/unit_tests/core/app/segments/test_variables.py +++ b/api/tests/unit_tests/core/variables/test_variables.py @@ -11,6 +11,7 @@ from core.variables import ( SegmentType, StringVariable, ) +from core.variables.variables import Variable def test_frozen_variables(): @@ -75,7 +76,7 @@ def test_object_variable_to_object(): def test_variable_to_object(): - var = StringVariable(name="text", value="text") + var: Variable = StringVariable(name="text", value="text") assert var.to_object() == "text" var = IntegerVariable(name="integer", value=42) assert var.to_object() == 42 diff --git a/api/tests/unit_tests/core/workflow/graph_engine/entities/test_graph_runtime_state.py b/api/tests/unit_tests/core/workflow/graph_engine/entities/test_graph_runtime_state.py new file mode 100644 index 0000000000..cf7cee8710 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/graph_engine/entities/test_graph_runtime_state.py @@ -0,0 +1,146 @@ +import time +from decimal import Decimal + +from core.model_runtime.entities.llm_entities import LLMUsage +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState +from core.workflow.graph_engine.entities.runtime_route_state import RuntimeRouteState +from core.workflow.system_variable import SystemVariable + + +def create_test_graph_runtime_state() -> GraphRuntimeState: + """Factory function to create a GraphRuntimeState with non-empty values for testing.""" + # Create a variable pool with system variables + system_vars = SystemVariable( + user_id="test_user_123", + app_id="test_app_456", + workflow_id="test_workflow_789", + workflow_execution_id="test_execution_001", + query="test query", + conversation_id="test_conv_123", + dialogue_count=5, + ) + variable_pool = VariablePool(system_variables=system_vars) + + # Add some variables to the variable pool + variable_pool.add(["test_node", "test_var"], "test_value") + variable_pool.add(["another_node", "another_var"], 42) + + # Create LLM usage with realistic values + llm_usage = LLMUsage( + prompt_tokens=150, + prompt_unit_price=Decimal("0.001"), + prompt_price_unit=Decimal(1000), + prompt_price=Decimal("0.15"), + completion_tokens=75, + completion_unit_price=Decimal("0.002"), + completion_price_unit=Decimal(1000), + completion_price=Decimal("0.15"), + total_tokens=225, + total_price=Decimal("0.30"), + currency="USD", + latency=1.25, + ) + + # Create runtime route state with some node states + node_run_state = RuntimeRouteState() + node_state = node_run_state.create_node_state("test_node_1") + node_run_state.add_route(node_state.id, "target_node_id") + + return GraphRuntimeState( + variable_pool=variable_pool, + start_at=time.perf_counter(), + total_tokens=100, + llm_usage=llm_usage, + outputs={ + "string_output": "test result", + "int_output": 42, + "float_output": 3.14, + "list_output": ["item1", "item2", "item3"], + "dict_output": {"key1": "value1", "key2": 123}, + "nested_dict": {"level1": {"level2": ["nested", "list", 456]}}, + }, + node_run_steps=5, + node_run_state=node_run_state, + ) + + +def test_basic_round_trip_serialization(): + """Test basic round-trip serialization ensures GraphRuntimeState values remain unchanged.""" + # Create a state with non-empty values + original_state = create_test_graph_runtime_state() + + # Serialize to JSON and deserialize back + json_data = original_state.model_dump_json() + deserialized_state = GraphRuntimeState.model_validate_json(json_data) + + # Core test: ensure the round-trip preserves all values + assert deserialized_state == original_state + + # Serialize to JSON and deserialize back + dict_data = original_state.model_dump(mode="python") + deserialized_state = GraphRuntimeState.model_validate(dict_data) + assert deserialized_state == original_state + + # Serialize to JSON and deserialize back + dict_data = original_state.model_dump(mode="json") + deserialized_state = GraphRuntimeState.model_validate(dict_data) + assert deserialized_state == original_state + + +def test_outputs_field_round_trip(): + """Test the problematic outputs field maintains values through round-trip serialization.""" + original_state = create_test_graph_runtime_state() + + # Serialize and deserialize + json_data = original_state.model_dump_json() + deserialized_state = GraphRuntimeState.model_validate_json(json_data) + + # Verify the outputs field specifically maintains its values + assert deserialized_state.outputs == original_state.outputs + assert deserialized_state == original_state + + +def test_empty_outputs_round_trip(): + """Test round-trip serialization with empty outputs field.""" + variable_pool = VariablePool.empty() + original_state = GraphRuntimeState( + variable_pool=variable_pool, + start_at=time.perf_counter(), + outputs={}, # Empty outputs + ) + + json_data = original_state.model_dump_json() + deserialized_state = GraphRuntimeState.model_validate_json(json_data) + + assert deserialized_state == original_state + + +def test_llm_usage_round_trip(): + # Create LLM usage with specific decimal values + llm_usage = LLMUsage( + prompt_tokens=100, + prompt_unit_price=Decimal("0.0015"), + prompt_price_unit=Decimal(1000), + prompt_price=Decimal("0.15"), + completion_tokens=50, + completion_unit_price=Decimal("0.003"), + completion_price_unit=Decimal(1000), + completion_price=Decimal("0.15"), + total_tokens=150, + total_price=Decimal("0.30"), + currency="USD", + latency=2.5, + ) + + json_data = llm_usage.model_dump_json() + deserialized = LLMUsage.model_validate_json(json_data) + assert deserialized == llm_usage + + dict_data = llm_usage.model_dump(mode="python") + deserialized = LLMUsage.model_validate(dict_data) + assert deserialized == llm_usage + + dict_data = llm_usage.model_dump(mode="json") + deserialized = LLMUsage.model_validate(dict_data) + assert deserialized == llm_usage diff --git a/api/tests/unit_tests/core/workflow/graph_engine/entities/test_node_run_state.py b/api/tests/unit_tests/core/workflow/graph_engine/entities/test_node_run_state.py new file mode 100644 index 0000000000..f3de42479a --- /dev/null +++ b/api/tests/unit_tests/core/workflow/graph_engine/entities/test_node_run_state.py @@ -0,0 +1,401 @@ +import json +import uuid +from datetime import UTC, datetime + +import pytest +from pydantic import ValidationError + +from core.workflow.entities.node_entities import NodeRunResult +from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus +from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeState, RuntimeRouteState + +_TEST_DATETIME = datetime(2024, 1, 15, 10, 30, 45) + + +class TestRouteNodeStateSerialization: + """Test cases for RouteNodeState Pydantic serialization/deserialization.""" + + def _test_route_node_state(self): + """Test comprehensive RouteNodeState serialization with all core fields validation.""" + + node_run_result = NodeRunResult( + status=WorkflowNodeExecutionStatus.SUCCEEDED, + inputs={"input_key": "input_value"}, + outputs={"output_key": "output_value"}, + ) + + node_state = RouteNodeState( + node_id="comprehensive_test_node", + start_at=_TEST_DATETIME, + finished_at=_TEST_DATETIME, + status=RouteNodeState.Status.SUCCESS, + node_run_result=node_run_result, + index=5, + paused_at=_TEST_DATETIME, + paused_by="user_123", + failed_reason="test_reason", + ) + return node_state + + def test_route_node_state_comprehensive_field_validation(self): + """Test comprehensive RouteNodeState serialization with all core fields validation.""" + node_state = self._test_route_node_state() + serialized = node_state.model_dump() + + # Comprehensive validation of all RouteNodeState fields + assert serialized["node_id"] == "comprehensive_test_node" + assert serialized["status"] == RouteNodeState.Status.SUCCESS + assert serialized["start_at"] == _TEST_DATETIME + assert serialized["finished_at"] == _TEST_DATETIME + assert serialized["paused_at"] == _TEST_DATETIME + assert serialized["paused_by"] == "user_123" + assert serialized["failed_reason"] == "test_reason" + assert serialized["index"] == 5 + assert "id" in serialized + assert isinstance(serialized["id"], str) + uuid.UUID(serialized["id"]) # Validate UUID format + + # Validate nested NodeRunResult structure + assert serialized["node_run_result"] is not None + assert serialized["node_run_result"]["status"] == WorkflowNodeExecutionStatus.SUCCEEDED + assert serialized["node_run_result"]["inputs"] == {"input_key": "input_value"} + assert serialized["node_run_result"]["outputs"] == {"output_key": "output_value"} + + def test_route_node_state_minimal_required_fields(self): + """Test RouteNodeState with only required fields, focusing on defaults.""" + node_state = RouteNodeState(node_id="minimal_node", start_at=_TEST_DATETIME) + + serialized = node_state.model_dump() + + # Focus on required fields and default values (not re-testing all fields) + assert serialized["node_id"] == "minimal_node" + assert serialized["start_at"] == _TEST_DATETIME + assert serialized["status"] == RouteNodeState.Status.RUNNING # Default status + assert serialized["index"] == 1 # Default index + assert serialized["node_run_result"] is None # Default None + json = node_state.model_dump_json() + deserialized = RouteNodeState.model_validate_json(json) + assert deserialized == node_state + + def test_route_node_state_deserialization_from_dict(self): + """Test RouteNodeState deserialization from dictionary data.""" + test_datetime = datetime(2024, 1, 15, 10, 30, 45) + test_id = str(uuid.uuid4()) + + dict_data = { + "id": test_id, + "node_id": "deserialized_node", + "start_at": test_datetime, + "status": "success", + "finished_at": test_datetime, + "index": 3, + } + + node_state = RouteNodeState.model_validate(dict_data) + + # Focus on deserialization accuracy + assert node_state.id == test_id + assert node_state.node_id == "deserialized_node" + assert node_state.start_at == test_datetime + assert node_state.status == RouteNodeState.Status.SUCCESS + assert node_state.finished_at == test_datetime + assert node_state.index == 3 + + def test_route_node_state_round_trip_consistency(self): + node_states = ( + self._test_route_node_state(), + RouteNodeState(node_id="minimal_node", start_at=_TEST_DATETIME), + ) + for node_state in node_states: + json = node_state.model_dump_json() + deserialized = RouteNodeState.model_validate_json(json) + assert deserialized == node_state + + dict_ = node_state.model_dump(mode="python") + deserialized = RouteNodeState.model_validate(dict_) + assert deserialized == node_state + + dict_ = node_state.model_dump(mode="json") + deserialized = RouteNodeState.model_validate(dict_) + assert deserialized == node_state + + +class TestRouteNodeStateEnumSerialization: + """Dedicated tests for RouteNodeState Status enum serialization behavior.""" + + def test_status_enum_model_dump_behavior(self): + """Test Status enum serialization in model_dump() returns enum objects.""" + + for status_enum in RouteNodeState.Status: + node_state = RouteNodeState(node_id="enum_test", start_at=_TEST_DATETIME, status=status_enum) + serialized = node_state.model_dump(mode="python") + assert serialized["status"] == status_enum + serialized = node_state.model_dump(mode="json") + assert serialized["status"] == status_enum.value + + def test_status_enum_json_serialization_behavior(self): + """Test Status enum serialization in JSON returns string values.""" + test_datetime = datetime(2024, 1, 15, 10, 30, 45) + + enum_to_string_mapping = { + RouteNodeState.Status.RUNNING: "running", + RouteNodeState.Status.SUCCESS: "success", + RouteNodeState.Status.FAILED: "failed", + RouteNodeState.Status.PAUSED: "paused", + RouteNodeState.Status.EXCEPTION: "exception", + } + + for status_enum, expected_string in enum_to_string_mapping.items(): + node_state = RouteNodeState(node_id="json_enum_test", start_at=test_datetime, status=status_enum) + + json_data = json.loads(node_state.model_dump_json()) + assert json_data["status"] == expected_string + + def test_status_enum_deserialization_from_string(self): + """Test Status enum deserialization from string values.""" + test_datetime = datetime(2024, 1, 15, 10, 30, 45) + + string_to_enum_mapping = { + "running": RouteNodeState.Status.RUNNING, + "success": RouteNodeState.Status.SUCCESS, + "failed": RouteNodeState.Status.FAILED, + "paused": RouteNodeState.Status.PAUSED, + "exception": RouteNodeState.Status.EXCEPTION, + } + + for status_string, expected_enum in string_to_enum_mapping.items(): + dict_data = { + "node_id": "enum_deserialize_test", + "start_at": test_datetime, + "status": status_string, + } + + node_state = RouteNodeState.model_validate(dict_data) + assert node_state.status == expected_enum + + +class TestRuntimeRouteStateSerialization: + """Test cases for RuntimeRouteState Pydantic serialization/deserialization.""" + + _NODE1_ID = "node_1" + _ROUTE_STATE1_ID = str(uuid.uuid4()) + _NODE2_ID = "node_2" + _ROUTE_STATE2_ID = str(uuid.uuid4()) + _NODE3_ID = "node_3" + _ROUTE_STATE3_ID = str(uuid.uuid4()) + + def _get_runtime_route_state(self): + # Create node states with different configurations + node_state_1 = RouteNodeState( + id=self._ROUTE_STATE1_ID, + node_id=self._NODE1_ID, + start_at=_TEST_DATETIME, + index=1, + ) + node_state_2 = RouteNodeState( + id=self._ROUTE_STATE2_ID, + node_id=self._NODE2_ID, + start_at=_TEST_DATETIME, + status=RouteNodeState.Status.SUCCESS, + finished_at=_TEST_DATETIME, + index=2, + ) + node_state_3 = RouteNodeState( + id=self._ROUTE_STATE3_ID, + node_id=self._NODE3_ID, + start_at=_TEST_DATETIME, + status=RouteNodeState.Status.FAILED, + failed_reason="Test failure", + index=3, + ) + + runtime_state = RuntimeRouteState( + routes={node_state_1.id: [node_state_2.id, node_state_3.id], node_state_2.id: [node_state_3.id]}, + node_state_mapping={ + node_state_1.id: node_state_1, + node_state_2.id: node_state_2, + node_state_3.id: node_state_3, + }, + ) + + return runtime_state + + def test_runtime_route_state_comprehensive_structure_validation(self): + """Test comprehensive RuntimeRouteState serialization with full structure validation.""" + + runtime_state = self._get_runtime_route_state() + serialized = runtime_state.model_dump() + + # Comprehensive validation of RuntimeRouteState structure + assert "routes" in serialized + assert "node_state_mapping" in serialized + assert isinstance(serialized["routes"], dict) + assert isinstance(serialized["node_state_mapping"], dict) + + # Validate routes dictionary structure and content + assert len(serialized["routes"]) == 2 + assert self._ROUTE_STATE1_ID in serialized["routes"] + assert self._ROUTE_STATE2_ID in serialized["routes"] + assert serialized["routes"][self._ROUTE_STATE1_ID] == [self._ROUTE_STATE2_ID, self._ROUTE_STATE3_ID] + assert serialized["routes"][self._ROUTE_STATE2_ID] == [self._ROUTE_STATE3_ID] + + # Validate node_state_mapping dictionary structure and content + assert len(serialized["node_state_mapping"]) == 3 + for state_id in [ + self._ROUTE_STATE1_ID, + self._ROUTE_STATE2_ID, + self._ROUTE_STATE3_ID, + ]: + assert state_id in serialized["node_state_mapping"] + node_data = serialized["node_state_mapping"][state_id] + node_state = runtime_state.node_state_mapping[state_id] + assert node_data["node_id"] == node_state.node_id + assert node_data["status"] == node_state.status + assert node_data["index"] == node_state.index + + def test_runtime_route_state_empty_collections(self): + """Test RuntimeRouteState with empty collections, focusing on default behavior.""" + runtime_state = RuntimeRouteState() + serialized = runtime_state.model_dump() + + # Focus on default empty collection behavior + assert serialized["routes"] == {} + assert serialized["node_state_mapping"] == {} + assert isinstance(serialized["routes"], dict) + assert isinstance(serialized["node_state_mapping"], dict) + + def test_runtime_route_state_json_serialization_structure(self): + """Test RuntimeRouteState JSON serialization structure.""" + node_state = RouteNodeState(node_id="json_node", start_at=_TEST_DATETIME) + + runtime_state = RuntimeRouteState( + routes={"source": ["target1", "target2"]}, node_state_mapping={node_state.id: node_state} + ) + + json_str = runtime_state.model_dump_json() + json_data = json.loads(json_str) + + # Focus on JSON structure validation + assert isinstance(json_str, str) + assert isinstance(json_data, dict) + assert "routes" in json_data + assert "node_state_mapping" in json_data + assert json_data["routes"]["source"] == ["target1", "target2"] + assert node_state.id in json_data["node_state_mapping"] + + def test_runtime_route_state_deserialization_from_dict(self): + """Test RuntimeRouteState deserialization from dictionary data.""" + node_id = str(uuid.uuid4()) + + dict_data = { + "routes": {"source_node": ["target_node_1", "target_node_2"]}, + "node_state_mapping": { + node_id: { + "id": node_id, + "node_id": "test_node", + "start_at": _TEST_DATETIME, + "status": "running", + "index": 1, + } + }, + } + + runtime_state = RuntimeRouteState.model_validate(dict_data) + + # Focus on deserialization accuracy + assert runtime_state.routes == {"source_node": ["target_node_1", "target_node_2"]} + assert len(runtime_state.node_state_mapping) == 1 + assert node_id in runtime_state.node_state_mapping + + deserialized_node = runtime_state.node_state_mapping[node_id] + assert deserialized_node.node_id == "test_node" + assert deserialized_node.status == RouteNodeState.Status.RUNNING + assert deserialized_node.index == 1 + + def test_runtime_route_state_round_trip_consistency(self): + """Test RuntimeRouteState round-trip serialization consistency.""" + original = self._get_runtime_route_state() + + # Dictionary round trip + dict_data = original.model_dump(mode="python") + reconstructed = RuntimeRouteState.model_validate(dict_data) + assert reconstructed == original + + dict_data = original.model_dump(mode="json") + reconstructed = RuntimeRouteState.model_validate(dict_data) + assert reconstructed == original + + # JSON round trip + json_str = original.model_dump_json() + json_reconstructed = RuntimeRouteState.model_validate_json(json_str) + assert json_reconstructed == original + + +class TestSerializationEdgeCases: + """Test edge cases and error conditions for serialization/deserialization.""" + + def test_invalid_status_deserialization(self): + """Test deserialization with invalid status values.""" + test_datetime = _TEST_DATETIME + invalid_data = { + "node_id": "invalid_test", + "start_at": test_datetime, + "status": "invalid_status", + } + + with pytest.raises(ValidationError) as exc_info: + RouteNodeState.model_validate(invalid_data) + assert "status" in str(exc_info.value) + + def test_missing_required_fields_deserialization(self): + """Test deserialization with missing required fields.""" + incomplete_data = {"id": str(uuid.uuid4())} + + with pytest.raises(ValidationError) as exc_info: + RouteNodeState.model_validate(incomplete_data) + error_str = str(exc_info.value) + assert "node_id" in error_str or "start_at" in error_str + + def test_invalid_datetime_deserialization(self): + """Test deserialization with invalid datetime values.""" + invalid_data = { + "node_id": "datetime_test", + "start_at": "invalid_datetime", + "status": "running", + } + + with pytest.raises(ValidationError) as exc_info: + RouteNodeState.model_validate(invalid_data) + assert "start_at" in str(exc_info.value) + + def test_invalid_routes_structure_deserialization(self): + """Test RuntimeRouteState deserialization with invalid routes structure.""" + invalid_data = { + "routes": "invalid_routes_structure", # Should be dict + "node_state_mapping": {}, + } + + with pytest.raises(ValidationError) as exc_info: + RuntimeRouteState.model_validate(invalid_data) + assert "routes" in str(exc_info.value) + + def test_timezone_handling_in_datetime_fields(self): + """Test timezone handling in datetime field serialization.""" + utc_datetime = datetime.now(UTC) + naive_datetime = utc_datetime.replace(tzinfo=None) + + node_state = RouteNodeState(node_id="timezone_test", start_at=naive_datetime) + dict_ = node_state.model_dump() + + assert dict_["start_at"] == naive_datetime + + # Test round trip + reconstructed = RouteNodeState.model_validate(dict_) + assert reconstructed.start_at == naive_datetime + assert reconstructed.start_at.tzinfo is None + + json = node_state.model_dump_json() + + reconstructed = RouteNodeState.model_validate_json(json) + assert reconstructed.start_at == naive_datetime + assert reconstructed.start_at.tzinfo is None diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py b/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py index 7535ec4866..ed4e42425e 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_graph_engine.py @@ -1,3 +1,4 @@ +import time from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.node_entities import NodeRunResult, WorkflowNodeExecutionMetadataKey from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.event import ( BaseNodeEvent, GraphRunFailedEvent, @@ -19,12 +19,14 @@ from core.workflow.graph_engine.entities.event import ( NodeRunSucceededEvent, ) from core.workflow.graph_engine.entities.graph import Graph +from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeState from core.workflow.graph_engine.graph_engine import GraphEngine from core.workflow.nodes.code.code_node import CodeNode from core.workflow.nodes.event import RunCompletedEvent, RunStreamChunkEvent from core.workflow.nodes.llm.node import LLMNode from core.workflow.nodes.question_classifier.question_classifier_node import QuestionClassifierNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -169,9 +171,11 @@ def test_run_parallel_in_workflow(mock_close, mock_remove): graph = Graph.init(graph_config=graph_config) variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={"query": "hi"} + system_variables=SystemVariable(user_id="aaa", app_id="1", workflow_id="1", files=[]), + user_inputs={"query": "hi"}, ) + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) graph_engine = GraphEngine( tenant_id="111", app_id="222", @@ -183,7 +187,7 @@ def test_run_parallel_in_workflow(mock_close, mock_remove): invoke_from=InvokeFrom.WEB_APP, call_depth=0, graph=graph, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=500, max_execution_time=1200, ) @@ -290,15 +294,16 @@ def test_run_parallel_in_chatflow(mock_close, mock_remove): graph = Graph.init(graph_config=graph_config) variable_pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "what's the weather in SF", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, + system_variables=SystemVariable( + user_id="aaa", + files=[], + query="what's the weather in SF", + conversation_id="abababa", + ), user_inputs={}, ) + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) graph_engine = GraphEngine( tenant_id="111", app_id="222", @@ -310,7 +315,7 @@ def test_run_parallel_in_chatflow(mock_close, mock_remove): invoke_from=InvokeFrom.WEB_APP, call_depth=0, graph=graph, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=500, max_execution_time=1200, ) @@ -470,15 +475,16 @@ def test_run_branch(mock_close, mock_remove): graph = Graph.init(graph_config=graph_config) variable_pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "hi", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, + system_variables=SystemVariable( + user_id="aaa", + files=[], + query="hi", + conversation_id="abababa", + ), user_inputs={"uid": "takato"}, ) + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) graph_engine = GraphEngine( tenant_id="111", app_id="222", @@ -490,7 +496,7 @@ def test_run_branch(mock_close, mock_remove): invoke_from=InvokeFrom.WEB_APP, call_depth=0, graph=graph, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=500, max_execution_time=1200, ) @@ -799,20 +805,25 @@ def test_condition_parallel_correct_output(mock_close, mock_remove, app): # construct variable pool pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "dify", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "1", - }, + system_variables=SystemVariable( + user_id="1", + files=[], + query="dify", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], ) pool.add(["pe", "list_output"], ["dify-1", "dify-2"]) variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={"query": "hi"} + system_variables=SystemVariable( + user_id="aaa", + files=[], + ), + user_inputs={"query": "hi"}, ) + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) graph_engine = GraphEngine( tenant_id="111", app_id="222", @@ -824,7 +835,7 @@ def test_condition_parallel_correct_output(mock_close, mock_remove, app): invoke_from=InvokeFrom.WEB_APP, call_depth=0, graph=graph, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=500, max_execution_time=1200, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py index b7f78d91fa..85ff4f9c05 100644 --- a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py +++ b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py @@ -5,11 +5,11 @@ from unittest.mock import MagicMock from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.answer.answer_node import AnswerNode +from core.workflow.system_variable import SystemVariable from extensions.ext_database import db from models.enums import UserFrom from models.workflow import WorkflowType @@ -51,7 +51,7 @@ def test_execute_answer(): # construct variable pool pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], ) diff --git a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py index c3a3818655..137e8b889d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py +++ b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer_stream_processor.py @@ -3,7 +3,6 @@ from collections.abc import Generator from datetime import UTC, datetime from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.event import ( GraphEngineEvent, NodeRunStartedEvent, @@ -15,6 +14,7 @@ from core.workflow.graph_engine.entities.runtime_route_state import RouteNodeSta from core.workflow.nodes.answer.answer_stream_processor import AnswerStreamProcessor from core.workflow.nodes.enums import NodeType from core.workflow.nodes.start.entities import StartNodeData +from core.workflow.system_variable import SystemVariable def _recursive_process(graph: Graph, next_node_id: str) -> Generator[GraphEngineEvent, None, None]: @@ -180,12 +180,12 @@ def test_process(): graph = Graph.init(graph_config=graph_config) variable_pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "what's the weather in SF", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, + system_variables=SystemVariable( + user_id="aaa", + files=[], + query="what's the weather in SF", + conversation_id="abababa", + ), user_inputs={}, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py new file mode 100644 index 0000000000..8712b61a23 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py @@ -0,0 +1,36 @@ +from core.workflow.nodes.base.node import BaseNode +from core.workflow.nodes.enums import NodeType + +# Ensures that all node classes are imported. +from core.workflow.nodes.node_mapping import NODE_TYPE_CLASSES_MAPPING + +_ = NODE_TYPE_CLASSES_MAPPING + + +def _get_all_subclasses(root: type[BaseNode]) -> list[type[BaseNode]]: + subclasses = [] + queue = [root] + while queue: + cls = queue.pop() + + subclasses.extend(cls.__subclasses__()) + queue.extend(cls.__subclasses__()) + + return subclasses + + +def test_ensure_subclasses_of_base_node_has_node_type_and_version_method_defined(): + classes = _get_all_subclasses(BaseNode) # type: ignore + type_version_set: set[tuple[NodeType, str]] = set() + + for cls in classes: + # Validate that 'version' is directly defined in the class (not inherited) by checking the class's __dict__ + assert "version" in cls.__dict__, f"class {cls} should have version method defined (NOT INHERITED.)" + node_type = cls._node_type + node_version = cls.version() + + assert isinstance(cls._node_type, NodeType) + assert isinstance(node_version, str) + node_type_and_version = (node_type, node_version) + assert node_type_and_version not in type_version_set + type_version_set.add(node_type_and_version) diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index d066fc1e33..bb6d72f51e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -7,12 +7,13 @@ from core.workflow.nodes.http_request import ( ) from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout from core.workflow.nodes.http_request.executor import Executor +from core.workflow.system_variable import SystemVariable def test_executor_with_json_body_and_number_variable(): # Prepare the variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add(["pre_node_id", "number"], 42) @@ -65,7 +66,7 @@ def test_executor_with_json_body_and_number_variable(): def test_executor_with_json_body_and_object_variable(): # Prepare the variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) @@ -120,7 +121,7 @@ def test_executor_with_json_body_and_object_variable(): def test_executor_with_json_body_and_nested_object_variable(): # Prepare the variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) @@ -174,7 +175,7 @@ def test_executor_with_json_body_and_nested_object_variable(): def test_extract_selectors_from_template_with_newline(): - variable_pool = VariablePool() + variable_pool = VariablePool(system_variables=SystemVariable.empty()) variable_pool.add(("node_id", "custom_query"), "line1\nline2") node_data = HttpRequestNodeData( title="Test JSON Body with Nested Object Variable", @@ -201,7 +202,7 @@ def test_extract_selectors_from_template_with_newline(): def test_executor_with_form_data(): # Prepare the variable pool variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add(["pre_node_id", "text_field"], "Hello, World!") @@ -280,7 +281,11 @@ def test_init_headers(): authorization=HttpRequestNodeAuthorization(type="no-auth"), ) timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30) - return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool()) + return Executor( + node_data=node_data, + timeout=timeout, + variable_pool=VariablePool(system_variables=SystemVariable.empty()), + ) executor = create_executor("aa\n cc:") executor._init_headers() @@ -310,7 +315,11 @@ def test_init_params(): authorization=HttpRequestNodeAuthorization(type="no-auth"), ) timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30) - return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool()) + return Executor( + node_data=node_data, + timeout=timeout, + variable_pool=VariablePool(system_variables=SystemVariable.empty()), + ) # Test basic key-value pairs executor = create_executor("key1:value1\nkey2:value2") diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py index 7fd32a4826..33f9251a72 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py @@ -15,6 +15,7 @@ from core.workflow.nodes.http_request import ( HttpRequestNodeBody, HttpRequestNodeData, ) +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -40,7 +41,7 @@ def test_http_request_node_binary_file(monkeypatch): ), ) variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add( @@ -128,7 +129,7 @@ def test_http_request_node_form_with_file(monkeypatch): ), ) variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) variable_pool.add( @@ -223,7 +224,7 @@ def test_http_request_node_form_with_multiple_files(monkeypatch): ) variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py index 6d854c950d..17c23b7735 100644 --- a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py +++ b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration.py @@ -3,10 +3,10 @@ import uuid from unittest.mock import patch from core.app.entities.app_invoke_entities import InvokeFrom +from core.variables.segments import ArrayAnySegment, ArrayStringSegment from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState @@ -14,6 +14,7 @@ from core.workflow.nodes.event import RunCompletedEvent from core.workflow.nodes.iteration.entities import ErrorHandleMode from core.workflow.nodes.iteration.iteration_node import IterationNode from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -150,12 +151,12 @@ def test_run(): # construct variable pool pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "dify", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "1", - }, + system_variables=SystemVariable( + user_id="1", + files=[], + query="dify", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], ) @@ -197,7 +198,7 @@ def test_run(): count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert item.run_result.outputs == {"output": ArrayStringSegment(value=["dify 123", "dify 123"])} assert count == 20 @@ -367,12 +368,12 @@ def test_run_parallel(): # construct variable pool pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "dify", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "1", - }, + system_variables=SystemVariable( + user_id="1", + files=[], + query="dify", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], ) @@ -413,7 +414,7 @@ def test_run_parallel(): count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert item.run_result.outputs == {"output": ArrayStringSegment(value=["dify 123", "dify 123"])} assert count == 32 @@ -583,12 +584,12 @@ def test_iteration_run_in_parallel_mode(): # construct variable pool pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "dify", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "1", - }, + system_variables=SystemVariable( + user_id="1", + files=[], + query="dify", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], ) @@ -654,7 +655,7 @@ def test_iteration_run_in_parallel_mode(): parallel_arr.append(item) if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert item.run_result.outputs == {"output": ArrayStringSegment(value=["dify 123", "dify 123"])} assert count == 32 for item in sequential_result: @@ -662,7 +663,7 @@ def test_iteration_run_in_parallel_mode(): count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": ["dify 123", "dify 123"]} + assert item.run_result.outputs == {"output": ArrayStringSegment(value=["dify 123", "dify 123"])} assert count == 64 @@ -807,12 +808,12 @@ def test_iteration_run_error_handle(): # construct variable pool pool = VariablePool( - system_variables={ - SystemVariableKey.QUERY: "dify", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "1", - }, + system_variables=SystemVariable( + user_id="1", + files=[], + query="dify", + conversation_id="abababa", + ), user_inputs={}, environment_variables=[], ) @@ -846,7 +847,7 @@ def test_iteration_run_error_handle(): count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": [None, None]} + assert item.run_result.outputs == {"output": ArrayAnySegment(value=[None, None])} assert count == 14 # execute remove abnormal output @@ -857,5 +858,5 @@ def test_iteration_run_error_handle(): count += 1 if isinstance(item, RunCompletedEvent): assert item.run_result.status == WorkflowNodeExecutionStatus.SUCCEEDED - assert item.run_result.outputs == {"output": []} + assert item.run_result.outputs == {"output": ArrayAnySegment(value=[])} assert count == 14 diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 519dd73787..fefad0ec95 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -25,6 +25,7 @@ from core.workflow.entities.variable_pool import VariablePool from core.workflow.graph_engine import Graph, GraphInitParams, GraphRuntimeState from core.workflow.nodes.answer import AnswerStreamGenerateRoute from core.workflow.nodes.end import EndStreamParam +from core.workflow.nodes.llm import llm_utils from core.workflow.nodes.llm.entities import ( ContextConfig, LLMNodeChatModelMessage, @@ -35,6 +36,7 @@ from core.workflow.nodes.llm.entities import ( ) from core.workflow.nodes.llm.file_saver import LLMFileSaver from core.workflow.nodes.llm.node import LLMNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.provider import ProviderType from models.workflow import WorkflowType @@ -103,7 +105,7 @@ def graph() -> Graph: @pytest.fixture def graph_runtime_state() -> GraphRuntimeState: variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) return GraphRuntimeState( @@ -170,7 +172,7 @@ def model_config(): ) -def test_fetch_files_with_file_segment(llm_node): +def test_fetch_files_with_file_segment(): file = File( id="1", tenant_id="test", @@ -180,13 +182,14 @@ def test_fetch_files_with_file_segment(llm_node): related_id="1", storage_key="", ) - llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], file) + variable_pool = VariablePool.empty() + variable_pool.add(["sys", "files"], file) - result = llm_node._fetch_files(selector=["sys", "files"]) + result = llm_utils.fetch_files(variable_pool=variable_pool, selector=["sys", "files"]) assert result == [file] -def test_fetch_files_with_array_file_segment(llm_node): +def test_fetch_files_with_array_file_segment(): files = [ File( id="1", @@ -207,28 +210,32 @@ def test_fetch_files_with_array_file_segment(llm_node): storage_key="", ), ] - llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], ArrayFileSegment(value=files)) + variable_pool = VariablePool.empty() + variable_pool.add(["sys", "files"], ArrayFileSegment(value=files)) - result = llm_node._fetch_files(selector=["sys", "files"]) + result = llm_utils.fetch_files(variable_pool=variable_pool, selector=["sys", "files"]) assert result == files -def test_fetch_files_with_none_segment(llm_node): - llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], NoneSegment()) +def test_fetch_files_with_none_segment(): + variable_pool = VariablePool.empty() + variable_pool.add(["sys", "files"], NoneSegment()) - result = llm_node._fetch_files(selector=["sys", "files"]) + result = llm_utils.fetch_files(variable_pool=variable_pool, selector=["sys", "files"]) assert result == [] -def test_fetch_files_with_array_any_segment(llm_node): - llm_node.graph_runtime_state.variable_pool.add(["sys", "files"], ArrayAnySegment(value=[])) +def test_fetch_files_with_array_any_segment(): + variable_pool = VariablePool.empty() + variable_pool.add(["sys", "files"], ArrayAnySegment(value=[])) - result = llm_node._fetch_files(selector=["sys", "files"]) + result = llm_utils.fetch_files(variable_pool=variable_pool, selector=["sys", "files"]) assert result == [] -def test_fetch_files_with_non_existent_variable(llm_node): - result = llm_node._fetch_files(selector=["sys", "files"]) +def test_fetch_files_with_non_existent_variable(): + variable_pool = VariablePool.empty() + result = llm_utils.fetch_files(variable_pool=variable_pool, selector=["sys", "files"]) assert result == [] diff --git a/api/tests/unit_tests/core/workflow/nodes/test_answer.py b/api/tests/unit_tests/core/workflow/nodes/test_answer.py index abc822e98b..44c31b212e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_answer.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_answer.py @@ -5,11 +5,11 @@ from unittest.mock import MagicMock from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.answer.answer_node import AnswerNode +from core.workflow.system_variable import SystemVariable from extensions.ext_database import db from models.enums import UserFrom from models.workflow import WorkflowType @@ -53,7 +53,7 @@ def test_execute_answer(): # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], conversation_variables=[], diff --git a/api/tests/unit_tests/core/workflow/nodes/test_continue_on_error.py b/api/tests/unit_tests/core/workflow/nodes/test_continue_on_error.py index ff60d5974b..3f83428834 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_continue_on_error.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_continue_on_error.py @@ -1,9 +1,10 @@ +import time from unittest.mock import patch from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.entities.node_entities import NodeRunResult, WorkflowNodeExecutionMetadataKey +from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.event import ( GraphRunPartialSucceededEvent, NodeRunExceptionEvent, @@ -11,9 +12,11 @@ from core.workflow.graph_engine.entities.event import ( NodeRunStreamChunkEvent, ) from core.workflow.graph_engine.entities.graph import Graph +from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.graph_engine.graph_engine import GraphEngine from core.workflow.nodes.event.event import RunCompletedEvent, RunStreamChunkEvent from core.workflow.nodes.llm.node import LLMNode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -163,15 +166,16 @@ class ContinueOnErrorTestHelper: def create_test_graph_engine(graph_config: dict, user_inputs: dict | None = None): """Helper method to create a graph engine instance for testing""" graph = Graph.init(graph_config=graph_config) - variable_pool = { - "system_variables": { - SystemVariableKey.QUERY: "clear", - SystemVariableKey.FILES: [], - SystemVariableKey.CONVERSATION_ID: "abababa", - SystemVariableKey.USER_ID: "aaa", - }, - "user_inputs": user_inputs or {"uid": "takato"}, - } + variable_pool = VariablePool( + system_variables=SystemVariable( + user_id="aaa", + files=[], + query="clear", + conversation_id="abababa", + ), + user_inputs=user_inputs or {"uid": "takato"}, + ) + graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) return GraphEngine( tenant_id="111", @@ -184,7 +188,7 @@ class ContinueOnErrorTestHelper: invoke_from=InvokeFrom.WEB_APP, call_depth=0, graph=graph, - variable_pool=variable_pool, + graph_runtime_state=graph_runtime_state, max_execution_steps=500, max_execution_time=1200, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py index 4cb1aa93f9..66c7818adf 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py @@ -7,6 +7,7 @@ from docx.oxml.text.paragraph import CT_P from core.file import File, FileTransferMethod from core.variables import ArrayFileSegment +from core.variables.segments import ArrayStringSegment from core.variables.variables import StringVariable from core.workflow.entities.node_entities import NodeRunResult from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus @@ -69,7 +70,13 @@ def test_run_invalid_variable_type(document_extractor_node, mock_graph_runtime_s @pytest.mark.parametrize( ("mime_type", "file_content", "expected_text", "transfer_method", "extension"), [ - ("text/plain", b"Hello, world!", ["Hello, world!"], FileTransferMethod.LOCAL_FILE, ".txt"), + ( + "text/plain", + b"Hello, world!", + ["Hello, world!"], + FileTransferMethod.LOCAL_FILE, + ".txt", + ), ( "application/pdf", b"%PDF-1.5\n%Test PDF content", @@ -84,7 +91,13 @@ def test_run_invalid_variable_type(document_extractor_node, mock_graph_runtime_s FileTransferMethod.REMOTE_URL, "", ), - ("text/plain", b"Remote content", ["Remote content"], FileTransferMethod.REMOTE_URL, None), + ( + "text/plain", + b"Remote content", + ["Remote content"], + FileTransferMethod.REMOTE_URL, + None, + ), ], ) def test_run_extract_text( @@ -131,7 +144,7 @@ def test_run_extract_text( assert isinstance(result, NodeRunResult) assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED, result.error assert result.outputs is not None - assert result.outputs["text"] == expected_text + assert result.outputs["text"] == ArrayStringSegment(value=expected_text) if transfer_method == FileTransferMethod.REMOTE_URL: mock_ssrf_proxy_get.assert_called_once_with("https://example.com/file.txt") @@ -329,3 +342,26 @@ def test_extract_text_from_excel_all_sheets_fail(mock_excel_file): assert result == "" assert mock_excel_instance.parse.call_count == 2 + + +@patch("pandas.ExcelFile") +def test_extract_text_from_excel_numeric_type_column(mock_excel_file): + """Test extracting text from Excel file with numeric column names.""" + + # Test numeric type column + data = {1: ["Test"], 1.1: ["Test"]} + + df = pd.DataFrame(data) + + # Mock ExcelFile + mock_excel_instance = Mock() + mock_excel_instance.sheet_names = ["Sheet1"] + mock_excel_instance.parse.return_value = df + mock_excel_file.return_value = mock_excel_instance + + file_content = b"fake_excel_content" + result = _extract_text_from_excel(file_content) + + expected_manual = "| 1.0 | 1.1 |\n| --- | --- |\n| Test | Test |\n\n" + + assert expected_manual == result diff --git a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py index c4e411f9d6..167a92484d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py @@ -7,12 +7,12 @@ from core.file import File, FileTransferMethod, FileType from core.variables import ArrayFileSegment from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionStatus -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.if_else.entities import IfElseNodeData from core.workflow.nodes.if_else.if_else_node import IfElseNode +from core.workflow.system_variable import SystemVariable from core.workflow.utils.condition.entities import Condition, SubCondition, SubVariableCondition from extensions.ext_database import db from models.enums import UserFrom @@ -37,9 +37,7 @@ def test_execute_if_else_result_true(): ) # construct variable pool - pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, user_inputs={} - ) + pool = VariablePool(system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}) pool.add(["start", "array_contains"], ["ab", "def"]) pool.add(["start", "array_not_contains"], ["ac", "def"]) pool.add(["start", "contains"], "cabcde") @@ -157,7 +155,7 @@ def test_execute_if_else_result_false(): # construct variable pool pool = VariablePool( - system_variables={SystemVariableKey.FILES: [], SystemVariableKey.USER_ID: "aaa"}, + system_variables=SystemVariable(user_id="aaa", files=[]), user_inputs={}, environment_variables=[], ) diff --git a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py index 77d42e2692..7d3a1d6a2d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py @@ -115,7 +115,7 @@ def test_filter_files_by_type(list_operator_node): }, ] assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED - for expected_file, result_file in zip(expected_files, result.outputs["result"]): + for expected_file, result_file in zip(expected_files, result.outputs["result"].value): assert expected_file["filename"] == result_file.filename assert expected_file["type"] == result_file.type assert expected_file["tenant_id"] == result_file.tenant_id diff --git a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py index e121f6338c..2776e57777 100644 --- a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py @@ -15,6 +15,7 @@ from core.workflow.nodes.enums import ErrorStrategy from core.workflow.nodes.event import RunCompletedEvent from core.workflow.nodes.tool import ToolNode from core.workflow.nodes.tool.entities import ToolNodeData +from core.workflow.system_variable import SystemVariable from models import UserFrom, WorkflowType @@ -34,7 +35,7 @@ def _create_tool_node(): version="1", ) variable_pool = VariablePool( - system_variables={}, + system_variables=SystemVariable.empty(), user_inputs={}, ) node = ToolNode( diff --git a/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py index 9793da129d..62e3e37104 100644 --- a/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py +++ b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v1/test_variable_assigner_v1.py @@ -5,13 +5,14 @@ from uuid import uuid4 from core.app.entities.app_invoke_entities import InvokeFrom from core.variables import ArrayStringVariable, StringVariable +from core.workflow.conversation_variable_updater import ConversationVariableUpdater from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.variable_assigner.v1 import VariableAssignerNode from core.workflow.nodes.variable_assigner.v1.node_data import WriteMode +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -63,10 +64,11 @@ def test_overwrite_string_variable(): name="test_string_variable", value="the second value", ) + conversation_id = str(uuid.uuid4()) # construct variable pool variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id=conversation_id), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], @@ -77,6 +79,9 @@ def test_overwrite_string_variable(): input_variable, ) + mock_conv_var_updater = mock.Mock(spec=ConversationVariableUpdater) + mock_conv_var_updater_factory = mock.Mock(return_value=mock_conv_var_updater) + node = VariableAssignerNode( id=str(uuid.uuid4()), graph_init_params=init_params, @@ -91,11 +96,20 @@ def test_overwrite_string_variable(): "input_variable_selector": [DEFAULT_NODE_ID, input_variable.name], }, }, + conv_var_updater_factory=mock_conv_var_updater_factory, ) - with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: - list(node.run()) - mock_run.assert_called_once() + list(node.run()) + expected_var = StringVariable( + id=conversation_variable.id, + name=conversation_variable.name, + description=conversation_variable.description, + selector=conversation_variable.selector, + value_type=conversation_variable.value_type, + value=input_variable.value, + ) + mock_conv_var_updater.update.assert_called_once_with(conversation_id=conversation_id, variable=expected_var) + mock_conv_var_updater.flush.assert_called_once() got = variable_pool.get(["conversation", conversation_variable.name]) assert got is not None @@ -148,9 +162,10 @@ def test_append_variable_to_array(): name="test_string_variable", value="the second value", ) + conversation_id = str(uuid.uuid4()) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id=conversation_id), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], @@ -160,6 +175,9 @@ def test_append_variable_to_array(): input_variable, ) + mock_conv_var_updater = mock.Mock(spec=ConversationVariableUpdater) + mock_conv_var_updater_factory = mock.Mock(return_value=mock_conv_var_updater) + node = VariableAssignerNode( id=str(uuid.uuid4()), graph_init_params=init_params, @@ -174,11 +192,22 @@ def test_append_variable_to_array(): "input_variable_selector": [DEFAULT_NODE_ID, input_variable.name], }, }, + conv_var_updater_factory=mock_conv_var_updater_factory, ) - with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: - list(node.run()) - mock_run.assert_called_once() + list(node.run()) + expected_value = list(conversation_variable.value) + expected_value.append(input_variable.value) + expected_var = ArrayStringVariable( + id=conversation_variable.id, + name=conversation_variable.name, + description=conversation_variable.description, + selector=conversation_variable.selector, + value_type=conversation_variable.value_type, + value=expected_value, + ) + mock_conv_var_updater.update.assert_called_once_with(conversation_id=conversation_id, variable=expected_var) + mock_conv_var_updater.flush.assert_called_once() got = variable_pool.get(["conversation", conversation_variable.name]) assert got is not None @@ -225,13 +254,17 @@ def test_clear_array(): value=["the first value"], ) + conversation_id = str(uuid.uuid4()) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id=conversation_id), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], ) + mock_conv_var_updater = mock.Mock(spec=ConversationVariableUpdater) + mock_conv_var_updater_factory = mock.Mock(return_value=mock_conv_var_updater) + node = VariableAssignerNode( id=str(uuid.uuid4()), graph_init_params=init_params, @@ -246,11 +279,20 @@ def test_clear_array(): "input_variable_selector": [], }, }, + conv_var_updater_factory=mock_conv_var_updater_factory, ) - with mock.patch("core.workflow.nodes.variable_assigner.common.helpers.update_conversation_variable") as mock_run: - list(node.run()) - mock_run.assert_called_once() + list(node.run()) + expected_var = ArrayStringVariable( + id=conversation_variable.id, + name=conversation_variable.name, + description=conversation_variable.description, + selector=conversation_variable.selector, + value_type=conversation_variable.value_type, + value=[], + ) + mock_conv_var_updater.update.assert_called_once_with(conversation_id=conversation_id, variable=expected_var) + mock_conv_var_updater.flush.assert_called_once() got = variable_pool.get(["conversation", conversation_variable.name]) assert got is not None diff --git a/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py index 7c5597dd89..a3a90b0599 100644 --- a/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py +++ b/api/tests/unit_tests/core/workflow/nodes/variable_assigner/v2/test_variable_assigner_v2.py @@ -5,12 +5,12 @@ from uuid import uuid4 from core.app.entities.app_invoke_entities import InvokeFrom from core.variables import ArrayStringVariable from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey from core.workflow.graph_engine.entities.graph import Graph from core.workflow.graph_engine.entities.graph_init_params import GraphInitParams from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState from core.workflow.nodes.variable_assigner.v2 import VariableAssignerNode from core.workflow.nodes.variable_assigner.v2.enums import InputType, Operation +from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from models.workflow import WorkflowType @@ -109,7 +109,7 @@ def test_remove_first_from_array(): ) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id="conversation_id"), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], @@ -196,7 +196,7 @@ def test_remove_last_from_array(): ) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id="conversation_id"), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], @@ -275,7 +275,7 @@ def test_remove_first_from_empty_array(): ) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id="conversation_id"), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], @@ -354,7 +354,7 @@ def test_remove_last_from_empty_array(): ) variable_pool = VariablePool( - system_variables={SystemVariableKey.CONVERSATION_ID: "conversation_id"}, + system_variables=SystemVariable(conversation_id="conversation_id"), user_inputs={}, environment_variables=[], conversation_variables=[conversation_variable], diff --git a/api/tests/unit_tests/core/workflow/test_system_variable.py b/api/tests/unit_tests/core/workflow/test_system_variable.py new file mode 100644 index 0000000000..11d788ed79 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/test_system_variable.py @@ -0,0 +1,251 @@ +import json +from typing import Any + +import pytest +from pydantic import ValidationError + +from core.file.enums import FileTransferMethod, FileType +from core.file.models import File +from core.workflow.system_variable import SystemVariable + +# Test data constants for SystemVariable serialization tests +VALID_BASE_DATA: dict[str, Any] = { + "user_id": "a20f06b1-8703-45ab-937c-860a60072113", + "app_id": "661bed75-458d-49c9-b487-fda0762677b9", + "workflow_id": "d31f2136-b292-4ae0-96d4-1e77894a4f43", +} + +COMPLETE_VALID_DATA: dict[str, Any] = { + **VALID_BASE_DATA, + "query": "test query", + "files": [], + "conversation_id": "91f1eb7d-69f4-4d7b-b82f-4003d51744b9", + "dialogue_count": 5, + "workflow_run_id": "eb4704b5-2274-47f2-bfcd-0452daa82cb5", +} + + +def create_test_file() -> File: + """Create a test File object for serialization tests.""" + return File( + tenant_id="test-tenant-id", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="test-file-id", + filename="test.txt", + extension=".txt", + mime_type="text/plain", + size=1024, + storage_key="test-storage-key", + ) + + +class TestSystemVariableSerialization: + """Focused tests for SystemVariable serialization/deserialization logic.""" + + def test_basic_deserialization(self): + """Test successful deserialization from JSON structure with all fields correctly mapped.""" + # Test with complete data + system_var = SystemVariable(**COMPLETE_VALID_DATA) + + # Verify all fields are correctly mapped + assert system_var.user_id == COMPLETE_VALID_DATA["user_id"] + assert system_var.app_id == COMPLETE_VALID_DATA["app_id"] + assert system_var.workflow_id == COMPLETE_VALID_DATA["workflow_id"] + assert system_var.query == COMPLETE_VALID_DATA["query"] + assert system_var.conversation_id == COMPLETE_VALID_DATA["conversation_id"] + assert system_var.dialogue_count == COMPLETE_VALID_DATA["dialogue_count"] + assert system_var.workflow_execution_id == COMPLETE_VALID_DATA["workflow_run_id"] + assert system_var.files == [] + + # Test with minimal data (only required fields) + minimal_var = SystemVariable(**VALID_BASE_DATA) + assert minimal_var.user_id == VALID_BASE_DATA["user_id"] + assert minimal_var.app_id == VALID_BASE_DATA["app_id"] + assert minimal_var.workflow_id == VALID_BASE_DATA["workflow_id"] + assert minimal_var.query is None + assert minimal_var.conversation_id is None + assert minimal_var.dialogue_count is None + assert minimal_var.workflow_execution_id is None + assert minimal_var.files == [] + + def test_alias_handling(self): + """Test workflow_execution_id vs workflow_run_id alias resolution - core deserialization logic.""" + workflow_id = "eb4704b5-2274-47f2-bfcd-0452daa82cb5" + + # Test workflow_run_id only (preferred alias) + data_run_id = {**VALID_BASE_DATA, "workflow_run_id": workflow_id} + system_var1 = SystemVariable(**data_run_id) + assert system_var1.workflow_execution_id == workflow_id + + # Test workflow_execution_id only (direct field name) + data_execution_id = {**VALID_BASE_DATA, "workflow_execution_id": workflow_id} + system_var2 = SystemVariable(**data_execution_id) + assert system_var2.workflow_execution_id == workflow_id + + # Test both present - workflow_run_id should take precedence + data_both = { + **VALID_BASE_DATA, + "workflow_execution_id": "should-be-ignored", + "workflow_run_id": workflow_id, + } + system_var3 = SystemVariable(**data_both) + assert system_var3.workflow_execution_id == workflow_id + + # Test neither present - should be None + system_var4 = SystemVariable(**VALID_BASE_DATA) + assert system_var4.workflow_execution_id is None + + def test_serialization_round_trip(self): + """Test that serialize → deserialize produces the same result with alias handling.""" + # Create original SystemVariable + original = SystemVariable(**COMPLETE_VALID_DATA) + + # Serialize to dict + serialized = original.model_dump(mode="json") + + # Verify alias is used in serialization (workflow_run_id, not workflow_execution_id) + assert "workflow_run_id" in serialized + assert "workflow_execution_id" not in serialized + assert serialized["workflow_run_id"] == COMPLETE_VALID_DATA["workflow_run_id"] + + # Deserialize back + deserialized = SystemVariable(**serialized) + + # Verify all fields match after round-trip + assert deserialized.user_id == original.user_id + assert deserialized.app_id == original.app_id + assert deserialized.workflow_id == original.workflow_id + assert deserialized.query == original.query + assert deserialized.conversation_id == original.conversation_id + assert deserialized.dialogue_count == original.dialogue_count + assert deserialized.workflow_execution_id == original.workflow_execution_id + assert list(deserialized.files) == list(original.files) + + def test_json_round_trip(self): + """Test JSON serialization/deserialization consistency with proper structure.""" + # Create original SystemVariable + original = SystemVariable(**COMPLETE_VALID_DATA) + + # Serialize to JSON string + json_str = original.model_dump_json() + + # Parse JSON and verify structure + json_data = json.loads(json_str) + assert "workflow_run_id" in json_data + assert "workflow_execution_id" not in json_data + assert json_data["workflow_run_id"] == COMPLETE_VALID_DATA["workflow_run_id"] + + # Deserialize from JSON data + deserialized = SystemVariable(**json_data) + + # Verify key fields match after JSON round-trip + assert deserialized.workflow_execution_id == original.workflow_execution_id + assert deserialized.user_id == original.user_id + assert deserialized.app_id == original.app_id + assert deserialized.workflow_id == original.workflow_id + + def test_files_field_deserialization(self): + """Test deserialization with File objects in the files field - SystemVariable specific logic.""" + # Test with empty files list + data_empty = {**VALID_BASE_DATA, "files": []} + system_var_empty = SystemVariable(**data_empty) + assert system_var_empty.files == [] + + # Test with single File object + test_file = create_test_file() + data_single = {**VALID_BASE_DATA, "files": [test_file]} + system_var_single = SystemVariable(**data_single) + assert len(system_var_single.files) == 1 + assert system_var_single.files[0].filename == "test.txt" + assert system_var_single.files[0].tenant_id == "test-tenant-id" + + # Test with multiple File objects + file1 = File( + tenant_id="tenant1", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="file1", + filename="doc1.txt", + storage_key="key1", + ) + file2 = File( + tenant_id="tenant2", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://example.com/image.jpg", + filename="image.jpg", + storage_key="key2", + ) + + data_multiple = {**VALID_BASE_DATA, "files": [file1, file2]} + system_var_multiple = SystemVariable(**data_multiple) + assert len(system_var_multiple.files) == 2 + assert system_var_multiple.files[0].filename == "doc1.txt" + assert system_var_multiple.files[1].filename == "image.jpg" + + # Verify files field serialization/deserialization + serialized = system_var_multiple.model_dump(mode="json") + deserialized = SystemVariable(**serialized) + assert len(deserialized.files) == 2 + assert deserialized.files[0].filename == "doc1.txt" + assert deserialized.files[1].filename == "image.jpg" + + def test_alias_serialization_consistency(self): + """Test that alias handling works consistently in both serialization directions.""" + workflow_id = "test-workflow-id" + + # Create with workflow_run_id (alias) + data_with_alias = {**VALID_BASE_DATA, "workflow_run_id": workflow_id} + system_var = SystemVariable(**data_with_alias) + + # Serialize and verify alias is used + serialized = system_var.model_dump() + assert serialized["workflow_run_id"] == workflow_id + assert "workflow_execution_id" not in serialized + + # Deserialize and verify field mapping + deserialized = SystemVariable(**serialized) + assert deserialized.workflow_execution_id == workflow_id + + # Test JSON serialization path + json_serialized = json.loads(system_var.model_dump_json()) + assert json_serialized["workflow_run_id"] == workflow_id + assert "workflow_execution_id" not in json_serialized + + json_deserialized = SystemVariable(**json_serialized) + assert json_deserialized.workflow_execution_id == workflow_id + + def test_model_validator_serialization_logic(self): + """Test the custom model validator behavior for serialization scenarios.""" + workflow_id = "test-workflow-execution-id" + + # Test direct instantiation with workflow_execution_id (should work) + data1 = {**VALID_BASE_DATA, "workflow_execution_id": workflow_id} + system_var1 = SystemVariable(**data1) + assert system_var1.workflow_execution_id == workflow_id + + # Test serialization of the above (should use alias) + serialized1 = system_var1.model_dump() + assert "workflow_run_id" in serialized1 + assert serialized1["workflow_run_id"] == workflow_id + + # Test both present - workflow_run_id takes precedence (validator logic) + data2 = { + **VALID_BASE_DATA, + "workflow_execution_id": "should-be-removed", + "workflow_run_id": workflow_id, + } + system_var2 = SystemVariable(**data2) + assert system_var2.workflow_execution_id == workflow_id + + # Verify serialization consistency + serialized2 = system_var2.model_dump() + assert serialized2["workflow_run_id"] == workflow_id + + +def test_constructor_with_extra_key(): + # Test that SystemVariable should forbid extra keys + with pytest.raises(ValidationError): + # This should fail because there is an unexpected key. + SystemVariable(invalid_key=1) # type: ignore diff --git a/api/tests/unit_tests/core/workflow/test_variable_pool.py b/api/tests/unit_tests/core/workflow/test_variable_pool.py index efbcdc760c..c65b60cb4d 100644 --- a/api/tests/unit_tests/core/workflow/test_variable_pool.py +++ b/api/tests/unit_tests/core/workflow/test_variable_pool.py @@ -1,13 +1,43 @@ +import uuid +from collections import defaultdict + import pytest from core.file import File, FileTransferMethod, FileType from core.variables import FileSegment, StringSegment +from core.variables.segments import ( + ArrayAnySegment, + ArrayFileSegment, + ArrayNumberSegment, + ArrayObjectSegment, + ArrayStringSegment, + FloatSegment, + IntegerSegment, + NoneSegment, + ObjectSegment, +) +from core.variables.variables import ( + ArrayNumberVariable, + ArrayObjectVariable, + ArrayStringVariable, + FloatVariable, + IntegerVariable, + ObjectVariable, + StringVariable, + VariableUnion, +) +from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from core.workflow.entities.variable_pool import VariablePool +from core.workflow.system_variable import SystemVariable +from factories.variable_factory import build_segment, segment_to_variable @pytest.fixture def pool(): - return VariablePool(system_variables={}, user_inputs={}) + return VariablePool( + system_variables=SystemVariable(user_id="test_user_id", app_id="test_app_id", workflow_id="test_workflow_id"), + user_inputs={}, + ) @pytest.fixture @@ -44,3 +74,365 @@ def test_use_long_selector(pool): result = pool.get(("node_1", "part_1", "part_2")) assert result is not None assert result.value == "test_value" + + +class TestVariablePool: + def test_constructor(self): + # Test with minimal required SystemVariable + minimal_system_vars = SystemVariable( + user_id="test_user_id", app_id="test_app_id", workflow_id="test_workflow_id" + ) + pool = VariablePool(system_variables=minimal_system_vars) + + # Test with all parameters + pool = VariablePool( + variable_dictionary={}, + user_inputs={}, + system_variables=minimal_system_vars, + environment_variables=[], + conversation_variables=[], + ) + + # Test with more complex SystemVariable + complex_system_vars = SystemVariable( + user_id="test_user_id", app_id="test_app_id", workflow_id="test_workflow_id" + ) + pool = VariablePool( + user_inputs={"key": "value"}, + system_variables=complex_system_vars, + environment_variables=[ + segment_to_variable( + segment=build_segment(1), + selector=[ENVIRONMENT_VARIABLE_NODE_ID, "env_var_1"], + name="env_var_1", + ) + ], + conversation_variables=[ + segment_to_variable( + segment=build_segment("1"), + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_var_1"], + name="conv_var_1", + ) + ], + ) + + def test_get_system_variables(self): + sys_var = SystemVariable( + user_id="test_user_id", + app_id="test_app_id", + workflow_id="test_workflow_id", + workflow_execution_id="test_execution_123", + query="test query", + conversation_id="test_conv_id", + dialogue_count=5, + ) + pool = VariablePool(system_variables=sys_var) + + kv = [ + ("user_id", sys_var.user_id), + ("app_id", sys_var.app_id), + ("workflow_id", sys_var.workflow_id), + ("workflow_run_id", sys_var.workflow_execution_id), + ("query", sys_var.query), + ("conversation_id", sys_var.conversation_id), + ("dialogue_count", sys_var.dialogue_count), + ] + for key, expected_value in kv: + segment = pool.get([SYSTEM_VARIABLE_NODE_ID, key]) + assert segment is not None + assert segment.value == expected_value + + +class TestVariablePoolSerialization: + """Test cases for VariablePool serialization and deserialization using Pydantic's built-in methods. + + These tests focus exclusively on serialization/deserialization logic to ensure that + VariablePool data can be properly serialized to dictionaries/JSON and reconstructed + while preserving all data integrity. + """ + + _NODE1_ID = "node_1" + _NODE2_ID = "node_2" + _NODE3_ID = "node_3" + + def _create_pool_without_file(self): + # Create comprehensive system variables + system_vars = SystemVariable( + user_id="test_user_id", + app_id="test_app_id", + workflow_id="test_workflow_id", + workflow_execution_id="test_execution_123", + query="test query", + conversation_id="test_conv_id", + dialogue_count=5, + ) + + # Create environment variables with all types including ArrayFileVariable + env_vars: list[VariableUnion] = [ + StringVariable( + id="env_string_id", + name="env_string", + value="env_string_value", + selector=[ENVIRONMENT_VARIABLE_NODE_ID, "env_string"], + ), + IntegerVariable( + id="env_integer_id", + name="env_integer", + value=1, + selector=[ENVIRONMENT_VARIABLE_NODE_ID, "env_integer"], + ), + FloatVariable( + id="env_float_id", + name="env_float", + value=1.0, + selector=[ENVIRONMENT_VARIABLE_NODE_ID, "env_float"], + ), + ] + + # Create conversation variables with complex data + conv_vars: list[VariableUnion] = [ + StringVariable( + id="conv_string_id", + name="conv_string", + value="conv_string_value", + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_string"], + ), + IntegerVariable( + id="conv_integer_id", + name="conv_integer", + value=1, + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_integer"], + ), + FloatVariable( + id="conv_float_id", + name="conv_float", + value=1.0, + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_float"], + ), + ObjectVariable( + id="conv_object_id", + name="conv_object", + value={"key": "value", "nested": {"data": 123}}, + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_object"], + ), + ArrayStringVariable( + id="conv_array_string_id", + name="conv_array_string", + value=["conv_array_string_value"], + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_array_string"], + ), + ArrayNumberVariable( + id="conv_array_number_id", + name="conv_array_number", + value=[1, 1.0], + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_array_number"], + ), + ArrayObjectVariable( + id="conv_array_object_id", + name="conv_array_object", + value=[{"a": 1}, {"b": "2"}], + selector=[CONVERSATION_VARIABLE_NODE_ID, "conv_array_object"], + ), + ] + + # Create comprehensive user inputs + user_inputs = { + "string_input": "test_value", + "number_input": 42, + "object_input": {"nested": {"key": "value"}}, + "array_input": ["item1", "item2", "item3"], + } + + # Create VariablePool + pool = VariablePool( + system_variables=system_vars, + user_inputs=user_inputs, + environment_variables=env_vars, + conversation_variables=conv_vars, + ) + return pool + + def _add_node_data_to_pool(self, pool: VariablePool, with_file=False): + test_file = File( + tenant_id="test_tenant_id", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="test_related_id", + remote_url="test_url", + filename="test_file.txt", + storage_key="test_storage_key", + ) + + # Add various segment types to variable dictionary + pool.add((self._NODE1_ID, "string_var"), StringSegment(value="test_string")) + pool.add((self._NODE1_ID, "int_var"), IntegerSegment(value=123)) + pool.add((self._NODE1_ID, "float_var"), FloatSegment(value=45.67)) + pool.add((self._NODE1_ID, "object_var"), ObjectSegment(value={"test": "data"})) + if with_file: + pool.add((self._NODE1_ID, "file_var"), FileSegment(value=test_file)) + pool.add((self._NODE1_ID, "none_var"), NoneSegment()) + + # Add array segments including ArrayFileVariable + pool.add((self._NODE2_ID, "array_string"), ArrayStringSegment(value=["a", "b", "c"])) + pool.add((self._NODE2_ID, "array_number"), ArrayNumberSegment(value=[1, 2, 3])) + pool.add((self._NODE2_ID, "array_object"), ArrayObjectSegment(value=[{"a": 1}, {"b": 2}])) + if with_file: + pool.add((self._NODE2_ID, "array_file"), ArrayFileSegment(value=[test_file])) + pool.add((self._NODE2_ID, "array_any"), ArrayAnySegment(value=["mixed", 123, {"key": "value"}])) + + # Add nested variables + pool.add((self._NODE3_ID, "nested", "deep", "var"), StringSegment(value="deep_value")) + + def test_system_variables(self): + sys_vars = SystemVariable( + user_id="test_user_id", + app_id="test_app_id", + workflow_id="test_workflow_id", + workflow_execution_id="test_execution_123", + query="test query", + conversation_id="test_conv_id", + dialogue_count=5, + ) + pool = VariablePool(system_variables=sys_vars) + json = pool.model_dump_json() + pool2 = VariablePool.model_validate_json(json) + assert pool2.system_variables == sys_vars + + for mode in ["json", "python"]: + dict_ = pool.model_dump(mode=mode) + pool2 = VariablePool.model_validate(dict_) + assert pool2.system_variables == sys_vars + + def test_pool_without_file_vars(self): + pool = self._create_pool_without_file() + json = pool.model_dump_json() + pool2 = pool.model_validate_json(json) + assert pool2.system_variables == pool.system_variables + assert pool2.conversation_variables == pool.conversation_variables + assert pool2.environment_variables == pool.environment_variables + assert pool2.user_inputs == pool.user_inputs + assert pool2.variable_dictionary == pool.variable_dictionary + assert pool2 == pool + + def test_basic_dictionary_round_trip(self): + """Test basic round-trip serialization: model_dump() → model_validate()""" + # Create a comprehensive VariablePool with all data types + original_pool = self._create_pool_without_file() + self._add_node_data_to_pool(original_pool) + + # Serialize to dictionary using Pydantic's model_dump() + serialized_data = original_pool.model_dump() + + # Verify serialized data structure + assert isinstance(serialized_data, dict) + assert "system_variables" in serialized_data + assert "user_inputs" in serialized_data + assert "environment_variables" in serialized_data + assert "conversation_variables" in serialized_data + assert "variable_dictionary" in serialized_data + + # Deserialize back using Pydantic's model_validate() + reconstructed_pool = VariablePool.model_validate(serialized_data) + + # Verify data integrity is preserved + self._assert_pools_equal(original_pool, reconstructed_pool) + + def test_json_round_trip(self): + """Test JSON round-trip serialization: model_dump_json() → model_validate_json()""" + # Create a comprehensive VariablePool with all data types + original_pool = self._create_pool_without_file() + self._add_node_data_to_pool(original_pool) + + # Serialize to JSON string using Pydantic's model_dump_json() + json_data = original_pool.model_dump_json() + + # Verify JSON is valid string + assert isinstance(json_data, str) + assert len(json_data) > 0 + + # Deserialize back using Pydantic's model_validate_json() + reconstructed_pool = VariablePool.model_validate_json(json_data) + + # Verify data integrity is preserved + self._assert_pools_equal(original_pool, reconstructed_pool) + + def test_complex_data_serialization(self): + """Test serialization of complex data structures including ArrayFileVariable""" + original_pool = self._create_pool_without_file() + self._add_node_data_to_pool(original_pool, with_file=True) + + # Test dictionary round-trip + dict_data = original_pool.model_dump() + reconstructed_dict = VariablePool.model_validate(dict_data) + + # Test JSON round-trip + json_data = original_pool.model_dump_json() + reconstructed_json = VariablePool.model_validate_json(json_data) + + # Verify both reconstructed pools are equivalent + self._assert_pools_equal(reconstructed_dict, reconstructed_json) + # TODO: assert the data for file object... + + def _assert_pools_equal(self, pool1: VariablePool, pool2: VariablePool) -> None: + """Assert that two VariablePools contain equivalent data""" + + # Compare system variables + assert pool1.system_variables == pool2.system_variables + + # Compare user inputs + assert dict(pool1.user_inputs) == dict(pool2.user_inputs) + + # Compare environment variables count + assert pool1.environment_variables == pool2.environment_variables + + # Compare conversation variables count + assert pool1.conversation_variables == pool2.conversation_variables + + # Test key variable retrievals to ensure functionality is preserved + test_selectors = [ + (SYSTEM_VARIABLE_NODE_ID, "user_id"), + (SYSTEM_VARIABLE_NODE_ID, "app_id"), + (ENVIRONMENT_VARIABLE_NODE_ID, "env_string"), + (ENVIRONMENT_VARIABLE_NODE_ID, "env_number"), + (CONVERSATION_VARIABLE_NODE_ID, "conv_string"), + (self._NODE1_ID, "string_var"), + (self._NODE1_ID, "int_var"), + (self._NODE1_ID, "float_var"), + (self._NODE2_ID, "array_string"), + (self._NODE2_ID, "array_number"), + (self._NODE3_ID, "nested", "deep", "var"), + ] + + for selector in test_selectors: + val1 = pool1.get(selector) + val2 = pool2.get(selector) + + # Both should exist or both should be None + assert (val1 is None) == (val2 is None) + + if val1 is not None and val2 is not None: + # Values should be equal + assert val1.value == val2.value + # Value types should be the same (more important than exact class type) + assert val1.value_type == val2.value_type + + def test_variable_pool_deserialization_default_dict(self): + variable_pool = VariablePool( + user_inputs={"a": 1, "b": "2"}, + system_variables=SystemVariable(workflow_id=str(uuid.uuid4())), + environment_variables=[ + StringVariable(name="str_var", value="a"), + ], + conversation_variables=[IntegerVariable(name="int_var", value=1)], + ) + assert isinstance(variable_pool.variable_dictionary, defaultdict) + json = variable_pool.model_dump_json() + loaded = VariablePool.model_validate_json(json) + assert isinstance(loaded.variable_dictionary, defaultdict) + + loaded.add(["non_exist_node", "a"], 1) + + pool_dict = variable_pool.model_dump() + loaded = VariablePool.model_validate(pool_dict) + assert isinstance(loaded.variable_dictionary, defaultdict) + loaded.add(["non_exist_node", "a"], 1) diff --git a/api/tests/unit_tests/core/workflow/test_workflow_cycle_manager.py b/api/tests/unit_tests/core/workflow/test_workflow_cycle_manager.py index fddc182594..642bc810ba 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_cycle_manager.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_cycle_manager.py @@ -18,10 +18,10 @@ from core.workflow.entities.workflow_node_execution import ( WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus, ) -from core.workflow.enums import SystemVariableKey from core.workflow.nodes import NodeType from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository +from core.workflow.system_variable import SystemVariable from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager from models.enums import CreatorUserRole from models.model import AppMode @@ -67,14 +67,14 @@ def real_app_generate_entity(): @pytest.fixture def real_workflow_system_variables(): - return { - SystemVariableKey.QUERY: "test query", - SystemVariableKey.CONVERSATION_ID: "test-conversation-id", - SystemVariableKey.USER_ID: "test-user-id", - SystemVariableKey.APP_ID: "test-app-id", - SystemVariableKey.WORKFLOW_ID: "test-workflow-id", - SystemVariableKey.WORKFLOW_EXECUTION_ID: "test-workflow-run-id", - } + return SystemVariable( + query="test query", + conversation_id="test-conversation-id", + user_id="test-user-id", + app_id="test-app-id", + workflow_id="test-workflow-id", + workflow_execution_id="test-workflow-run-id", + ) @pytest.fixture @@ -163,7 +163,6 @@ def real_workflow_run(): workflow_run.tenant_id = "test-tenant-id" workflow_run.app_id = "test-app-id" workflow_run.workflow_id = "test-workflow-id" - workflow_run.sequence_number = 1 workflow_run.type = "chat" workflow_run.triggered_from = "app-run" workflow_run.version = "1.0" diff --git a/api/tests/unit_tests/core/workflow/utils/test_variable_template_parser.py b/api/tests/unit_tests/core/workflow/utils/test_variable_template_parser.py index 2f90afcf89..28ef05edde 100644 --- a/api/tests/unit_tests/core/workflow/utils/test_variable_template_parser.py +++ b/api/tests/unit_tests/core/workflow/utils/test_variable_template_parser.py @@ -1,22 +1,10 @@ -from core.variables import SecretVariable +import dataclasses + from core.workflow.entities.variable_entities import VariableSelector -from core.workflow.entities.variable_pool import VariablePool -from core.workflow.enums import SystemVariableKey from core.workflow.utils import variable_template_parser def test_extract_selectors_from_template(): - variable_pool = VariablePool( - system_variables={ - SystemVariableKey("user_id"): "fake-user-id", - }, - user_inputs={}, - environment_variables=[ - SecretVariable(name="secret_key", value="fake-secret-key"), - ], - conversation_variables=[], - ) - variable_pool.add(("node_id", "custom_query"), "fake-user-query") template = ( "Hello, {{#sys.user_id#}}! Your query is {{#node_id.custom_query#}}. And your key is {{#env.secret_key#}}." ) @@ -26,3 +14,35 @@ def test_extract_selectors_from_template(): VariableSelector(variable="#node_id.custom_query#", value_selector=["node_id", "custom_query"]), VariableSelector(variable="#env.secret_key#", value_selector=["env", "secret_key"]), ] + + +def test_invalid_references(): + @dataclasses.dataclass + class TestCase: + name: str + template: str + + cases = [ + TestCase( + name="lack of closing brace", + template="Hello, {{#sys.user_id#", + ), + TestCase( + name="lack of opening brace", + template="Hello, #sys.user_id#}}", + ), + TestCase( + name="lack selector name", + template="Hello, {{#sys#}}", + ), + TestCase( + name="empty node name part", + template="Hello, {{#.user_id#}}", + ), + ] + for idx, c in enumerate(cases, 1): + fail_msg = f"Test case {c.name} failed, index={idx}" + selectors = variable_template_parser.extract_selectors_from_template(c.template) + assert selectors == [], fail_msg + parser = variable_template_parser.VariableTemplateParser(c.template) + assert parser.extract_variable_selectors() == [], fail_msg diff --git a/api/tests/unit_tests/core/workflow/utils/test_variable_utils.py b/api/tests/unit_tests/core/workflow/utils/test_variable_utils.py new file mode 100644 index 0000000000..54bf6558bf --- /dev/null +++ b/api/tests/unit_tests/core/workflow/utils/test_variable_utils.py @@ -0,0 +1,148 @@ +from typing import Any + +from core.variables.segments import ObjectSegment, StringSegment +from core.workflow.entities.variable_pool import VariablePool +from core.workflow.utils.variable_utils import append_variables_recursively + + +class TestAppendVariablesRecursively: + """Test cases for append_variables_recursively function""" + + def test_append_simple_dict_value(self): + """Test appending a simple dictionary value""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["output"] + variable_value = {"name": "John", "age": 30} + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check that the main variable is added + main_var = pool.get([node_id] + variable_key_list) + assert main_var is not None + assert main_var.value == variable_value + + # Check that nested variables are added recursively + name_var = pool.get([node_id] + variable_key_list + ["name"]) + assert name_var is not None + assert name_var.value == "John" + + age_var = pool.get([node_id] + variable_key_list + ["age"]) + assert age_var is not None + assert age_var.value == 30 + + def test_append_object_segment_value(self): + """Test appending an ObjectSegment value""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["result"] + + # Create an ObjectSegment + obj_data = {"status": "success", "code": 200} + variable_value = ObjectSegment(value=obj_data) + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check that the main variable is added + main_var = pool.get([node_id] + variable_key_list) + assert main_var is not None + assert isinstance(main_var, ObjectSegment) + assert main_var.value == obj_data + + # Check that nested variables are added recursively + status_var = pool.get([node_id] + variable_key_list + ["status"]) + assert status_var is not None + assert status_var.value == "success" + + code_var = pool.get([node_id] + variable_key_list + ["code"]) + assert code_var is not None + assert code_var.value == 200 + + def test_append_nested_dict_value(self): + """Test appending a nested dictionary value""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["data"] + + variable_value = { + "user": { + "profile": {"name": "Alice", "email": "alice@example.com"}, + "settings": {"theme": "dark", "notifications": True}, + }, + "metadata": {"version": "1.0", "timestamp": 1234567890}, + } + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check deeply nested variables + name_var = pool.get([node_id] + variable_key_list + ["user", "profile", "name"]) + assert name_var is not None + assert name_var.value == "Alice" + + email_var = pool.get([node_id] + variable_key_list + ["user", "profile", "email"]) + assert email_var is not None + assert email_var.value == "alice@example.com" + + theme_var = pool.get([node_id] + variable_key_list + ["user", "settings", "theme"]) + assert theme_var is not None + assert theme_var.value == "dark" + + notifications_var = pool.get([node_id] + variable_key_list + ["user", "settings", "notifications"]) + assert notifications_var is not None + assert notifications_var.value == 1 # Boolean True is converted to integer 1 + + version_var = pool.get([node_id] + variable_key_list + ["metadata", "version"]) + assert version_var is not None + assert version_var.value == "1.0" + + def test_append_non_dict_value(self): + """Test appending a non-dictionary value (should not recurse)""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["simple"] + variable_value = "simple_string" + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check that only the main variable is added + main_var = pool.get([node_id] + variable_key_list) + assert main_var is not None + assert main_var.value == variable_value + + # Ensure no additional variables are created + assert len(pool.variable_dictionary[node_id]) == 1 + + def test_append_segment_non_object_value(self): + """Test appending a Segment that is not ObjectSegment (should not recurse)""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["text"] + variable_value = StringSegment(value="Hello World") + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check that only the main variable is added + main_var = pool.get([node_id] + variable_key_list) + assert main_var is not None + assert isinstance(main_var, StringSegment) + assert main_var.value == "Hello World" + + # Ensure no additional variables are created + assert len(pool.variable_dictionary[node_id]) == 1 + + def test_append_empty_dict_value(self): + """Test appending an empty dictionary value""" + pool = VariablePool.empty() + node_id = "test_node" + variable_key_list = ["empty"] + variable_value: dict[str, Any] = {} + + append_variables_recursively(pool, node_id, variable_key_list, variable_value) + + # Check that the main variable is added + main_var = pool.get([node_id] + variable_key_list) + assert main_var is not None + assert main_var.value == {} + + # Ensure only the main variable is created (no recursion for empty dict) + assert len(pool.variable_dictionary[node_id]) == 1 diff --git a/api/tests/unit_tests/extensions/test_redis.py b/api/tests/unit_tests/extensions/test_redis.py new file mode 100644 index 0000000000..933fa32894 --- /dev/null +++ b/api/tests/unit_tests/extensions/test_redis.py @@ -0,0 +1,53 @@ +from redis import RedisError + +from extensions.ext_redis import redis_fallback + + +def test_redis_fallback_success(): + @redis_fallback(default_return=None) + def test_func(): + return "success" + + assert test_func() == "success" + + +def test_redis_fallback_error(): + @redis_fallback(default_return="fallback") + def test_func(): + raise RedisError("Redis error") + + assert test_func() == "fallback" + + +def test_redis_fallback_none_default(): + @redis_fallback() + def test_func(): + raise RedisError("Redis error") + + assert test_func() is None + + +def test_redis_fallback_with_args(): + @redis_fallback(default_return=0) + def test_func(x, y): + raise RedisError("Redis error") + + assert test_func(1, 2) == 0 + + +def test_redis_fallback_with_kwargs(): + @redis_fallback(default_return={}) + def test_func(x=None, y=None): + raise RedisError("Redis error") + + assert test_func(x=1, y=2) == {} + + +def test_redis_fallback_preserves_function_metadata(): + @redis_fallback(default_return=None) + def test_func(): + """Test function docstring""" + pass + + assert test_func.__name__ == "test_func" + assert test_func.__doc__ == "Test function docstring" diff --git a/api/tests/unit_tests/factories/test_variable_factory.py b/api/tests/unit_tests/factories/test_variable_factory.py new file mode 100644 index 0000000000..4f2542a323 --- /dev/null +++ b/api/tests/unit_tests/factories/test_variable_factory.py @@ -0,0 +1,861 @@ +import math +from dataclasses import dataclass +from typing import Any +from uuid import uuid4 + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from core.file import File, FileTransferMethod, FileType +from core.variables import ( + ArrayNumberVariable, + ArrayObjectVariable, + ArrayStringVariable, + FloatVariable, + IntegerVariable, + SecretVariable, + StringVariable, +) +from core.variables.exc import VariableError +from core.variables.segments import ( + ArrayAnySegment, + ArrayFileSegment, + ArrayNumberSegment, + ArrayObjectSegment, + ArrayStringSegment, + FileSegment, + FloatSegment, + IntegerSegment, + NoneSegment, + ObjectSegment, + StringSegment, +) +from core.variables.types import SegmentType +from factories import variable_factory +from factories.variable_factory import TypeMismatchError, build_segment_with_type + + +def test_string_variable(): + test_data = {"value_type": "string", "name": "test_text", "value": "Hello, World!"} + result = variable_factory.build_conversation_variable_from_mapping(test_data) + assert isinstance(result, StringVariable) + + +def test_integer_variable(): + test_data = {"value_type": "number", "name": "test_int", "value": 42} + result = variable_factory.build_conversation_variable_from_mapping(test_data) + assert isinstance(result, IntegerVariable) + + +def test_float_variable(): + test_data = {"value_type": "number", "name": "test_float", "value": 3.14} + result = variable_factory.build_conversation_variable_from_mapping(test_data) + assert isinstance(result, FloatVariable) + + +def test_secret_variable(): + test_data = {"value_type": "secret", "name": "test_secret", "value": "secret_value"} + result = variable_factory.build_conversation_variable_from_mapping(test_data) + assert isinstance(result, SecretVariable) + + +def test_invalid_value_type(): + test_data = {"value_type": "unknown", "name": "test_invalid", "value": "value"} + with pytest.raises(VariableError): + variable_factory.build_conversation_variable_from_mapping(test_data) + + +def test_build_a_blank_string(): + result = variable_factory.build_conversation_variable_from_mapping( + { + "value_type": "string", + "name": "blank", + "value": "", + } + ) + assert isinstance(result, StringVariable) + assert result.value == "" + + +def test_build_a_object_variable_with_none_value(): + var = variable_factory.build_segment( + { + "key1": None, + } + ) + assert isinstance(var, ObjectSegment) + assert var.value["key1"] is None + + +def test_object_variable(): + mapping = { + "id": str(uuid4()), + "value_type": "object", + "name": "test_object", + "description": "Description of the variable.", + "value": { + "key1": "text", + "key2": 2, + }, + } + variable = variable_factory.build_conversation_variable_from_mapping(mapping) + assert isinstance(variable, ObjectSegment) + assert isinstance(variable.value["key1"], str) + assert isinstance(variable.value["key2"], int) + + +def test_array_string_variable(): + mapping = { + "id": str(uuid4()), + "value_type": "array[string]", + "name": "test_array", + "description": "Description of the variable.", + "value": [ + "text", + "text", + ], + } + variable = variable_factory.build_conversation_variable_from_mapping(mapping) + assert isinstance(variable, ArrayStringVariable) + assert isinstance(variable.value[0], str) + assert isinstance(variable.value[1], str) + + +def test_array_number_variable(): + mapping = { + "id": str(uuid4()), + "value_type": "array[number]", + "name": "test_array", + "description": "Description of the variable.", + "value": [ + 1, + 2.0, + ], + } + variable = variable_factory.build_conversation_variable_from_mapping(mapping) + assert isinstance(variable, ArrayNumberVariable) + assert isinstance(variable.value[0], int) + assert isinstance(variable.value[1], float) + + +def test_array_object_variable(): + mapping = { + "id": str(uuid4()), + "value_type": "array[object]", + "name": "test_array", + "description": "Description of the variable.", + "value": [ + { + "key1": "text", + "key2": 1, + }, + { + "key1": "text", + "key2": 1, + }, + ], + } + variable = variable_factory.build_conversation_variable_from_mapping(mapping) + assert isinstance(variable, ArrayObjectVariable) + assert isinstance(variable.value[0], dict) + assert isinstance(variable.value[1], dict) + assert isinstance(variable.value[0]["key1"], str) + assert isinstance(variable.value[0]["key2"], int) + assert isinstance(variable.value[1]["key1"], str) + assert isinstance(variable.value[1]["key2"], int) + + +def test_variable_cannot_large_than_200_kb(): + with pytest.raises(VariableError): + variable_factory.build_conversation_variable_from_mapping( + { + "id": str(uuid4()), + "value_type": "string", + "name": "test_text", + "value": "a" * 1024 * 201, + } + ) + + +def test_array_none_variable(): + var = variable_factory.build_segment([None, None, None, None]) + assert isinstance(var, ArrayAnySegment) + assert var.value == [None, None, None, None] + + +def test_build_segment_none_type(): + """Test building NoneSegment from None value.""" + segment = variable_factory.build_segment(None) + assert isinstance(segment, NoneSegment) + assert segment.value is None + assert segment.value_type == SegmentType.NONE + + +def test_build_segment_none_type_properties(): + """Test NoneSegment properties and methods.""" + segment = variable_factory.build_segment(None) + assert segment.text == "" + assert segment.log == "" + assert segment.markdown == "" + assert segment.to_object() is None + + +def test_build_segment_array_file_single_file(): + """Test building ArrayFileSegment from list with single file.""" + file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file.png", + filename="test-file", + extension=".png", + mime_type="image/png", + size=1000, + ) + segment = variable_factory.build_segment([file]) + assert isinstance(segment, ArrayFileSegment) + assert len(segment.value) == 1 + assert segment.value[0] == file + assert segment.value_type == SegmentType.ARRAY_FILE + + +def test_build_segment_array_file_multiple_files(): + """Test building ArrayFileSegment from list with multiple files.""" + file1 = File( + id="test_file_id_1", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file1.png", + filename="test-file1", + extension=".png", + mime_type="image/png", + size=1000, + ) + file2 = File( + id="test_file_id_2", + tenant_id="test_tenant_id", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="test_relation_id", + filename="test-file2", + extension=".txt", + mime_type="text/plain", + size=500, + ) + segment = variable_factory.build_segment([file1, file2]) + assert isinstance(segment, ArrayFileSegment) + assert len(segment.value) == 2 + assert segment.value[0] == file1 + assert segment.value[1] == file2 + assert segment.value_type == SegmentType.ARRAY_FILE + + +def test_build_segment_array_file_empty_list(): + """Test building ArrayFileSegment from empty list should create ArrayAnySegment.""" + segment = variable_factory.build_segment([]) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == [] + assert segment.value_type == SegmentType.ARRAY_ANY + + +def test_build_segment_array_any_mixed_types(): + """Test building ArrayAnySegment from list with mixed types.""" + mixed_values = ["string", 42, 3.14, {"key": "value"}, None] + segment = variable_factory.build_segment(mixed_values) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == mixed_values + assert segment.value_type == SegmentType.ARRAY_ANY + + +def test_build_segment_array_any_with_nested_arrays(): + """Test building ArrayAnySegment from list containing arrays.""" + nested_values = [["nested", "array"], [1, 2, 3], "string"] + segment = variable_factory.build_segment(nested_values) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == nested_values + assert segment.value_type == SegmentType.ARRAY_ANY + + +def test_build_segment_array_any_mixed_with_files(): + """Test building ArrayAnySegment from list with files and other types.""" + file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file.png", + filename="test-file", + extension=".png", + mime_type="image/png", + size=1000, + ) + mixed_values = [file, "string", 42] + segment = variable_factory.build_segment(mixed_values) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == mixed_values + assert segment.value_type == SegmentType.ARRAY_ANY + + +def test_build_segment_array_any_all_none_values(): + """Test building ArrayAnySegment from list with all None values.""" + none_values = [None, None, None] + segment = variable_factory.build_segment(none_values) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == none_values + assert segment.value_type == SegmentType.ARRAY_ANY + + +def test_build_segment_array_file_properties(): + """Test ArrayFileSegment properties and methods.""" + file1 = File( + id="test_file_id_1", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file1.png", + filename="test-file1", + extension=".png", + mime_type="image/png", + size=1000, + ) + file2 = File( + id="test_file_id_2", + tenant_id="test_tenant_id", + type=FileType.DOCUMENT, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file2.txt", + filename="test-file2", + extension=".txt", + mime_type="text/plain", + size=500, + ) + segment = variable_factory.build_segment([file1, file2]) + + # Test properties + assert segment.text == "" # ArrayFileSegment text property returns empty string + assert segment.log == "" # ArrayFileSegment log property returns empty string + assert segment.markdown == f"{file1.markdown}\n{file2.markdown}" + assert segment.to_object() == [file1, file2] + + +def test_build_segment_array_any_properties(): + """Test ArrayAnySegment properties and methods.""" + mixed_values = ["string", 42, None] + segment = variable_factory.build_segment(mixed_values) + + # Test properties + assert segment.text == str(mixed_values) + assert segment.log == str(mixed_values) + assert segment.markdown == "string\n42\nNone" + assert segment.to_object() == mixed_values + + +def test_build_segment_edge_cases(): + """Test edge cases for build_segment function.""" + # Test with complex nested structures + complex_structure = [{"nested": {"deep": [1, 2, 3]}}, [{"inner": "value"}], "mixed"] + segment = variable_factory.build_segment(complex_structure) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == complex_structure + + # Test with single None in list + single_none = [None] + segment = variable_factory.build_segment(single_none) + assert isinstance(segment, ArrayAnySegment) + assert segment.value == single_none + + +def test_build_segment_file_array_with_different_file_types(): + """Test ArrayFileSegment with different file types.""" + image_file = File( + id="image_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/image.png", + filename="image", + extension=".png", + mime_type="image/png", + size=1000, + ) + + video_file = File( + id="video_id", + tenant_id="test_tenant_id", + type=FileType.VIDEO, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="video_relation_id", + filename="video", + extension=".mp4", + mime_type="video/mp4", + size=5000, + ) + + audio_file = File( + id="audio_id", + tenant_id="test_tenant_id", + type=FileType.AUDIO, + transfer_method=FileTransferMethod.LOCAL_FILE, + related_id="audio_relation_id", + filename="audio", + extension=".mp3", + mime_type="audio/mpeg", + size=3000, + ) + + segment = variable_factory.build_segment([image_file, video_file, audio_file]) + assert isinstance(segment, ArrayFileSegment) + assert len(segment.value) == 3 + assert segment.value[0].type == FileType.IMAGE + assert segment.value[1].type == FileType.VIDEO + assert segment.value[2].type == FileType.AUDIO + + +@st.composite +def _generate_file(draw) -> File: + file_type, mime_type, extension = draw( + st.sampled_from( + [ + (FileType.IMAGE, "image/png", ".png"), + (FileType.VIDEO, "video/mp4", ".mp4"), + (FileType.DOCUMENT, "text/plain", ".txt"), + (FileType.AUDIO, "audio/mpeg", ".mp3"), + ] + ) + ) + filename = "test-file" + size = draw(st.integers(min_value=0, max_value=1024 * 1024)) + + transfer_method = draw(st.sampled_from(list(FileTransferMethod))) + if transfer_method == FileTransferMethod.REMOTE_URL: + url = "https://test.example.com/test-file" + file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=file_type, + transfer_method=transfer_method, + remote_url=url, + related_id=None, + filename=filename, + extension=extension, + mime_type=mime_type, + size=size, + ) + else: + relation_id = draw(st.uuids(version=4)) + + file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=file_type, + transfer_method=transfer_method, + related_id=str(relation_id), + filename=filename, + extension=extension, + mime_type=mime_type, + size=size, + ) + return file + + +def _scalar_value() -> st.SearchStrategy[int | float | str | File | None]: + return st.one_of( + st.none(), + st.integers(), + st.floats(), + st.text(), + _generate_file(), + ) + + +@given(_scalar_value()) +def test_build_segment_and_extract_values_for_scalar_types(value): + seg = variable_factory.build_segment(value) + # nan == nan yields false, so we need to use `math.isnan` to check `seg.value` here. + if isinstance(value, float) and math.isnan(value): + assert math.isnan(seg.value) + else: + assert seg.value == value + + +@given(st.lists(_scalar_value())) +def test_build_segment_and_extract_values_for_array_types(values): + seg = variable_factory.build_segment(values) + assert seg.value == values + + +def test_build_segment_type_for_scalar(): + @dataclass(frozen=True) + class TestCase: + value: int | float | str | File + expected_type: SegmentType + + file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file.png", + filename="test-file", + extension=".png", + mime_type="image/png", + size=1000, + ) + cases = [ + TestCase(0, SegmentType.INTEGER), + TestCase(0.0, SegmentType.FLOAT), + TestCase("", SegmentType.STRING), + TestCase(file, SegmentType.FILE), + ] + + for idx, c in enumerate(cases, 1): + segment = variable_factory.build_segment(c.value) + assert segment.value_type == c.expected_type, f"test case {idx} failed." + + +class TestBuildSegmentWithType: + """Test cases for build_segment_with_type function.""" + + def test_string_type(self): + """Test building a string segment with correct type.""" + result = build_segment_with_type(SegmentType.STRING, "hello") + assert isinstance(result, StringSegment) + assert result.value == "hello" + assert result.value_type == SegmentType.STRING + + def test_number_type_integer(self): + """Test building a number segment with integer value.""" + result = build_segment_with_type(SegmentType.NUMBER, 42) + assert isinstance(result, IntegerSegment) + assert result.value == 42 + assert result.value_type == SegmentType.INTEGER + + def test_number_type_float(self): + """Test building a number segment with float value.""" + result = build_segment_with_type(SegmentType.NUMBER, 3.14) + assert isinstance(result, FloatSegment) + assert result.value == 3.14 + assert result.value_type == SegmentType.FLOAT + + def test_object_type(self): + """Test building an object segment with correct type.""" + test_obj = {"key": "value", "nested": {"inner": 123}} + result = build_segment_with_type(SegmentType.OBJECT, test_obj) + assert isinstance(result, ObjectSegment) + assert result.value == test_obj + assert result.value_type == SegmentType.OBJECT + + def test_file_type(self): + """Test building a file segment with correct type.""" + test_file = File( + id="test_file_id", + tenant_id="test_tenant_id", + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://test.example.com/test-file.png", + filename="test-file", + extension=".png", + mime_type="image/png", + size=1000, + storage_key="test_storage_key", + ) + result = build_segment_with_type(SegmentType.FILE, test_file) + assert isinstance(result, FileSegment) + assert result.value == test_file + assert result.value_type == SegmentType.FILE + + def test_none_type(self): + """Test building a none segment with None value.""" + result = build_segment_with_type(SegmentType.NONE, None) + assert isinstance(result, NoneSegment) + assert result.value is None + assert result.value_type == SegmentType.NONE + + def test_empty_array_string(self): + """Test building an empty array[string] segment.""" + result = build_segment_with_type(SegmentType.ARRAY_STRING, []) + assert isinstance(result, ArrayStringSegment) + assert result.value == [] + assert result.value_type == SegmentType.ARRAY_STRING + + def test_empty_array_number(self): + """Test building an empty array[number] segment.""" + result = build_segment_with_type(SegmentType.ARRAY_NUMBER, []) + assert isinstance(result, ArrayNumberSegment) + assert result.value == [] + assert result.value_type == SegmentType.ARRAY_NUMBER + + def test_empty_array_object(self): + """Test building an empty array[object] segment.""" + result = build_segment_with_type(SegmentType.ARRAY_OBJECT, []) + assert isinstance(result, ArrayObjectSegment) + assert result.value == [] + assert result.value_type == SegmentType.ARRAY_OBJECT + + def test_empty_array_file(self): + """Test building an empty array[file] segment.""" + result = build_segment_with_type(SegmentType.ARRAY_FILE, []) + assert isinstance(result, ArrayFileSegment) + assert result.value == [] + assert result.value_type == SegmentType.ARRAY_FILE + + def test_empty_array_any(self): + """Test building an empty array[any] segment.""" + result = build_segment_with_type(SegmentType.ARRAY_ANY, []) + assert isinstance(result, ArrayAnySegment) + assert result.value == [] + assert result.value_type == SegmentType.ARRAY_ANY + + def test_array_with_values(self): + """Test building array segments with actual values.""" + # Array of strings + result = build_segment_with_type(SegmentType.ARRAY_STRING, ["hello", "world"]) + assert isinstance(result, ArrayStringSegment) + assert result.value == ["hello", "world"] + assert result.value_type == SegmentType.ARRAY_STRING + + # Array of numbers + result = build_segment_with_type(SegmentType.ARRAY_NUMBER, [1, 2, 3.14]) + assert isinstance(result, ArrayNumberSegment) + assert result.value == [1, 2, 3.14] + assert result.value_type == SegmentType.ARRAY_NUMBER + + # Array of objects + result = build_segment_with_type(SegmentType.ARRAY_OBJECT, [{"a": 1}, {"b": 2}]) + assert isinstance(result, ArrayObjectSegment) + assert result.value == [{"a": 1}, {"b": 2}] + assert result.value_type == SegmentType.ARRAY_OBJECT + + def test_type_mismatch_string_to_number(self): + """Test type mismatch when expecting number but getting string.""" + with pytest.raises(TypeMismatchError) as exc_info: + build_segment_with_type(SegmentType.NUMBER, "not_a_number") + + assert "Type mismatch" in str(exc_info.value) + assert "expected number" in str(exc_info.value) + assert "str" in str(exc_info.value) + + def test_type_mismatch_number_to_string(self): + """Test type mismatch when expecting string but getting number.""" + with pytest.raises(TypeMismatchError) as exc_info: + build_segment_with_type(SegmentType.STRING, 123) + + assert "Type mismatch" in str(exc_info.value) + assert "expected string" in str(exc_info.value) + assert "int" in str(exc_info.value) + + def test_type_mismatch_none_to_string(self): + """Test type mismatch when expecting string but getting None.""" + with pytest.raises(TypeMismatchError) as exc_info: + build_segment_with_type(SegmentType.STRING, None) + + assert "expected string, but got None" in str(exc_info.value) + + def test_type_mismatch_empty_list_to_non_array(self): + """Test type mismatch when expecting non-array type but getting empty list.""" + with pytest.raises(TypeMismatchError) as exc_info: + build_segment_with_type(SegmentType.STRING, []) + + assert "expected string, but got empty list" in str(exc_info.value) + + def test_type_mismatch_object_to_array(self): + """Test type mismatch when expecting array but getting object.""" + with pytest.raises(TypeMismatchError) as exc_info: + build_segment_with_type(SegmentType.ARRAY_STRING, {"key": "value"}) + + assert "Type mismatch" in str(exc_info.value) + assert "expected array[string]" in str(exc_info.value) + + def test_compatible_number_types(self): + """Test that int and float are both compatible with NUMBER type.""" + # Integer should work + result_int = build_segment_with_type(SegmentType.NUMBER, 42) + assert isinstance(result_int, IntegerSegment) + assert result_int.value_type == SegmentType.INTEGER + + # Float should work + result_float = build_segment_with_type(SegmentType.NUMBER, 3.14) + assert isinstance(result_float, FloatSegment) + assert result_float.value_type == SegmentType.FLOAT + + @pytest.mark.parametrize( + ("segment_type", "value", "expected_class"), + [ + (SegmentType.STRING, "test", StringSegment), + (SegmentType.INTEGER, 42, IntegerSegment), + (SegmentType.FLOAT, 3.14, FloatSegment), + (SegmentType.OBJECT, {}, ObjectSegment), + (SegmentType.NONE, None, NoneSegment), + (SegmentType.ARRAY_STRING, [], ArrayStringSegment), + (SegmentType.ARRAY_NUMBER, [], ArrayNumberSegment), + (SegmentType.ARRAY_OBJECT, [], ArrayObjectSegment), + (SegmentType.ARRAY_ANY, [], ArrayAnySegment), + ], + ) + def test_parametrized_valid_types(self, segment_type, value, expected_class): + """Parametrized test for valid type combinations.""" + result = build_segment_with_type(segment_type, value) + assert isinstance(result, expected_class) + assert result.value == value + assert result.value_type == segment_type + + @pytest.mark.parametrize( + ("segment_type", "value"), + [ + (SegmentType.STRING, 123), + (SegmentType.NUMBER, "not_a_number"), + (SegmentType.OBJECT, "not_an_object"), + (SegmentType.ARRAY_STRING, "not_an_array"), + (SegmentType.STRING, None), + (SegmentType.NUMBER, None), + ], + ) + def test_parametrized_type_mismatches(self, segment_type, value): + """Parametrized test for type mismatches that should raise TypeMismatchError.""" + with pytest.raises(TypeMismatchError): + build_segment_with_type(segment_type, value) + + +# Test cases for ValueError scenarios in build_segment function +class TestBuildSegmentValueErrors: + """Test cases for ValueError scenarios in the build_segment function.""" + + @dataclass(frozen=True) + class ValueErrorTestCase: + """Test case data for ValueError scenarios.""" + + name: str + description: str + test_value: Any + + def _get_test_cases(self): + """Get all test cases for ValueError scenarios.""" + + # Define inline classes for complex test cases + class CustomType: + pass + + def unsupported_function(): + return "test" + + def gen(): + yield 1 + yield 2 + + return [ + self.ValueErrorTestCase( + name="unsupported_custom_type", + description="custom class that doesn't match any supported type", + test_value=CustomType(), + ), + self.ValueErrorTestCase( + name="unsupported_set_type", + description="set (unsupported collection type)", + test_value={1, 2, 3}, + ), + self.ValueErrorTestCase( + name="unsupported_tuple_type", description="tuple (unsupported type)", test_value=(1, 2, 3) + ), + self.ValueErrorTestCase( + name="unsupported_bytes_type", + description="bytes (unsupported type)", + test_value=b"hello world", + ), + self.ValueErrorTestCase( + name="unsupported_function_type", + description="function (unsupported type)", + test_value=unsupported_function, + ), + self.ValueErrorTestCase( + name="unsupported_module_type", description="module (unsupported type)", test_value=math + ), + self.ValueErrorTestCase( + name="array_with_unsupported_element_types", + description="array with unsupported element types", + test_value=[CustomType()], + ), + self.ValueErrorTestCase( + name="mixed_array_with_unsupported_types", + description="array with mix of supported and unsupported types", + test_value=["valid_string", 42, CustomType()], + ), + self.ValueErrorTestCase( + name="nested_unsupported_types", + description="nested structures containing unsupported types", + test_value=[{"valid": "data"}, CustomType()], + ), + self.ValueErrorTestCase( + name="complex_number_type", + description="complex number (unsupported type)", + test_value=3 + 4j, + ), + self.ValueErrorTestCase( + name="range_type", description="range object (unsupported type)", test_value=range(10) + ), + self.ValueErrorTestCase( + name="generator_type", + description="generator (unsupported type)", + test_value=gen(), + ), + self.ValueErrorTestCase( + name="exception_message_contains_value", + description="set to verify error message contains the actual unsupported value", + test_value={1, 2, 3}, + ), + self.ValueErrorTestCase( + name="array_with_mixed_unsupported_segment_types", + description="array processing with unsupported segment types in match", + test_value=[CustomType()], + ), + self.ValueErrorTestCase( + name="frozenset_type", + description="frozenset (unsupported type)", + test_value=frozenset([1, 2, 3]), + ), + self.ValueErrorTestCase( + name="memoryview_type", + description="memoryview (unsupported type)", + test_value=memoryview(b"hello"), + ), + self.ValueErrorTestCase( + name="slice_type", description="slice object (unsupported type)", test_value=slice(1, 10, 2) + ), + self.ValueErrorTestCase(name="type_object", description="type object (unsupported type)", test_value=type), + self.ValueErrorTestCase( + name="generic_object", description="generic object (unsupported type)", test_value=object() + ), + ] + + def test_build_segment_unsupported_types(self): + """Table-driven test for all ValueError scenarios in build_segment function.""" + test_cases = self._get_test_cases() + + for index, test_case in enumerate(test_cases, 1): + # Use test value directly + test_value = test_case.test_value + + with pytest.raises(ValueError) as exc_info: # noqa: PT012 + segment = variable_factory.build_segment(test_value) + pytest.fail(f"Test case {index} ({test_case.name}) should raise ValueError but not, result={segment}") + + error_message = str(exc_info.value) + assert "not supported value" in error_message, ( + f"Test case {index} ({test_case.name}): Expected 'not supported value' in error message, " + f"but got: {error_message}" + ) + + def test_build_segment_boolean_type_note(self): + """Note: Boolean values are actually handled as integers in Python, so they don't raise ValueError.""" + # Boolean values in Python are subclasses of int, so they get processed as integers + # True becomes IntegerSegment(value=1) and False becomes IntegerSegment(value=0) + true_segment = variable_factory.build_segment(True) + false_segment = variable_factory.build_segment(False) + + # Verify they are processed as integers, not as errors + assert true_segment.value == 1, "Test case 1 (boolean_true): Expected True to be processed as integer 1" + assert false_segment.value == 0, "Test case 2 (boolean_false): Expected False to be processed as integer 0" + assert true_segment.value_type == SegmentType.INTEGER + assert false_segment.value_type == SegmentType.INTEGER diff --git a/api/tests/unit_tests/libs/test_datetime_utils.py b/api/tests/unit_tests/libs/test_datetime_utils.py new file mode 100644 index 0000000000..e7781a5821 --- /dev/null +++ b/api/tests/unit_tests/libs/test_datetime_utils.py @@ -0,0 +1,20 @@ +import datetime + +from libs.datetime_utils import naive_utc_now + + +def test_naive_utc_now(monkeypatch): + tz_aware_utc_now = datetime.datetime.now(tz=datetime.UTC) + + def _now_func(tz: datetime.timezone | None) -> datetime.datetime: + return tz_aware_utc_now.astimezone(tz) + + monkeypatch.setattr("libs.datetime_utils._now_func", _now_func) + + naive_datetime = naive_utc_now() + + assert naive_datetime.tzinfo is None + assert naive_datetime.date() == tz_aware_utc_now.date() + naive_time = naive_datetime.time() + utc_time = tz_aware_utc_now.time() + assert naive_time == utc_time diff --git a/api/tests/unit_tests/libs/test_flask_utils.py b/api/tests/unit_tests/libs/test_flask_utils.py new file mode 100644 index 0000000000..fb46ba50f3 --- /dev/null +++ b/api/tests/unit_tests/libs/test_flask_utils.py @@ -0,0 +1,124 @@ +import contextvars +import threading +from typing import Optional + +import pytest +from flask import Flask +from flask_login import LoginManager, UserMixin, current_user, login_user + +from libs.flask_utils import preserve_flask_contexts + + +class User(UserMixin): + """Simple User class for testing.""" + + def __init__(self, id: str): + self.id = id + + def get_id(self) -> str: + return self.id + + +@pytest.fixture +def login_app(app: Flask) -> Flask: + """Set up a Flask app with flask-login.""" + # Set a secret key for the app + app.config["SECRET_KEY"] = "test-secret-key" + + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(user_id: str) -> Optional[User]: + if user_id == "test_user": + return User("test_user") + return None + + return app + + +@pytest.fixture +def test_user() -> User: + """Create a test user.""" + return User("test_user") + + +def test_current_user_not_accessible_across_threads(login_app: Flask, test_user: User): + """ + Test that current_user is not accessible in a different thread without preserve_flask_contexts. + + This test demonstrates that without the preserve_flask_contexts, we cannot access + current_user in a different thread, even with app_context. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Store the result of the thread execution + result = {"user_accessible": True, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread(): + try: + # Try to access current_user in a different thread with app_context + with login_app.app_context(): + # This should fail because current_user is not accessible across threads + # without preserve_flask_contexts + result["user_accessible"] = current_user.is_authenticated + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread) + thread.start() + thread.join() + + # Verify that we got an error or current_user is not authenticated + assert result["error"] is not None or (result["user_accessible"] is not None and not result["user_accessible"]) + + +def test_current_user_accessible_with_preserve_flask_contexts(login_app: Flask, test_user: User): + """ + Test that current_user is accessible in a different thread with preserve_flask_contexts. + + This test demonstrates that with the preserve_flask_contexts, we can access + current_user in a different thread. + """ + # Log in the user in the main thread + with login_app.test_request_context(): + login_user(test_user) + assert current_user.is_authenticated + assert current_user.id == "test_user" + + # Save the context variables + context_vars = contextvars.copy_context() + + # Store the result of the thread execution + result = {"user_accessible": False, "user_id": None, "error": None} + + # Define a function to run in a separate thread + def check_user_in_thread_with_manager(): + try: + # Use preserve_flask_contexts to access current_user in a different thread + with preserve_flask_contexts(login_app, context_vars): + from flask_login import current_user + + if current_user: + result["user_accessible"] = True + result["user_id"] = current_user.id + else: + result["user_accessible"] = False + except Exception as e: + result["error"] = str(e) # type: ignore + + # Run the function in a separate thread + thread = threading.Thread(target=check_user_in_thread_with_manager) + thread.start() + thread.join() + + # Verify that current_user is accessible and has the correct ID + assert result["error"] is None + assert result["user_accessible"] is True + assert result["user_id"] == "test_user" diff --git a/api/tests/unit_tests/libs/test_helper.py b/api/tests/unit_tests/libs/test_helper.py new file mode 100644 index 0000000000..b7701055f5 --- /dev/null +++ b/api/tests/unit_tests/libs/test_helper.py @@ -0,0 +1,65 @@ +import pytest + +from libs.helper import extract_tenant_id +from models.account import Account +from models.model import EndUser + + +class TestExtractTenantId: + """Test cases for the extract_tenant_id utility function.""" + + def test_extract_tenant_id_from_account_with_tenant(self): + """Test extracting tenant_id from Account with current_tenant_id.""" + # Create a mock Account object + account = Account() + # Mock the current_tenant_id property + account._current_tenant = type("MockTenant", (), {"id": "account-tenant-123"})() + + tenant_id = extract_tenant_id(account) + assert tenant_id == "account-tenant-123" + + def test_extract_tenant_id_from_account_without_tenant(self): + """Test extracting tenant_id from Account without current_tenant_id.""" + # Create a mock Account object + account = Account() + account._current_tenant = None + + tenant_id = extract_tenant_id(account) + assert tenant_id is None + + def test_extract_tenant_id_from_enduser_with_tenant(self): + """Test extracting tenant_id from EndUser with tenant_id.""" + # Create a mock EndUser object + end_user = EndUser() + end_user.tenant_id = "enduser-tenant-456" + + tenant_id = extract_tenant_id(end_user) + assert tenant_id == "enduser-tenant-456" + + def test_extract_tenant_id_from_enduser_without_tenant(self): + """Test extracting tenant_id from EndUser without tenant_id.""" + # Create a mock EndUser object + end_user = EndUser() + end_user.tenant_id = None + + tenant_id = extract_tenant_id(end_user) + assert tenant_id is None + + def test_extract_tenant_id_with_invalid_user_type(self): + """Test extracting tenant_id with invalid user type raises ValueError.""" + invalid_user = "not_a_user_object" + + with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"): + extract_tenant_id(invalid_user) + + def test_extract_tenant_id_with_none_user(self): + """Test extracting tenant_id with None user raises ValueError.""" + with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"): + extract_tenant_id(None) + + def test_extract_tenant_id_with_dict_user(self): + """Test extracting tenant_id with dict user raises ValueError.""" + dict_user = {"id": "123", "tenant_id": "456"} + + with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"): + extract_tenant_id(dict_user) diff --git a/api/tests/unit_tests/libs/test_login.py b/api/tests/unit_tests/libs/test_login.py new file mode 100644 index 0000000000..39671077d4 --- /dev/null +++ b/api/tests/unit_tests/libs/test_login.py @@ -0,0 +1,232 @@ +from unittest.mock import MagicMock, patch + +import pytest +from flask import Flask, g +from flask_login import LoginManager, UserMixin + +from libs.login import _get_user, current_user, login_required + + +class MockUser(UserMixin): + """Mock user class for testing.""" + + def __init__(self, id: str, is_authenticated: bool = True): + self.id = id + self._is_authenticated = is_authenticated + + @property + def is_authenticated(self): + return self._is_authenticated + + +class TestLoginRequired: + """Test cases for login_required decorator.""" + + @pytest.fixture + def setup_app(self, app: Flask): + """Set up Flask app with login manager.""" + # Initialize login manager + login_manager = LoginManager() + login_manager.init_app(app) + + # Mock unauthorized handler + login_manager.unauthorized = MagicMock(return_value="Unauthorized") + + # Add a dummy user loader to prevent exceptions + @login_manager.user_loader + def load_user(user_id): + return None + + return app + + def test_authenticated_user_can_access_protected_view(self, setup_app: Flask): + """Test that authenticated users can access protected views.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock authenticated user + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + + def test_unauthenticated_user_cannot_access_protected_view(self, setup_app: Flask): + """Test that unauthenticated users are redirected.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock unauthenticated user + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Unauthorized" + setup_app.login_manager.unauthorized.assert_called_once() + + def test_login_disabled_allows_unauthenticated_access(self, setup_app: Flask): + """Test that LOGIN_DISABLED config bypasses authentication.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(): + # Mock unauthenticated user and LOGIN_DISABLED + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + with patch("libs.login.dify_config") as mock_config: + mock_config.LOGIN_DISABLED = True + + result = protected_view() + assert result == "Protected content" + # Ensure unauthorized was not called + setup_app.login_manager.unauthorized.assert_not_called() + + def test_options_request_bypasses_authentication(self, setup_app: Flask): + """Test that OPTIONS requests are exempt from authentication.""" + + @login_required + def protected_view(): + return "Protected content" + + with setup_app.test_request_context(method="OPTIONS"): + # Mock unauthenticated user + mock_user = MockUser("test_user", is_authenticated=False) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + # Ensure unauthorized was not called + setup_app.login_manager.unauthorized.assert_not_called() + + def test_flask_2_compatibility(self, setup_app: Flask): + """Test Flask 2.x compatibility with ensure_sync.""" + + @login_required + def protected_view(): + return "Protected content" + + # Mock Flask 2.x ensure_sync + setup_app.ensure_sync = MagicMock(return_value=lambda: "Synced content") + + with setup_app.test_request_context(): + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Synced content" + setup_app.ensure_sync.assert_called_once() + + def test_flask_1_compatibility(self, setup_app: Flask): + """Test Flask 1.x compatibility without ensure_sync.""" + + @login_required + def protected_view(): + return "Protected content" + + # Remove ensure_sync to simulate Flask 1.x + if hasattr(setup_app, "ensure_sync"): + delattr(setup_app, "ensure_sync") + + with setup_app.test_request_context(): + mock_user = MockUser("test_user", is_authenticated=True) + with patch("libs.login._get_user", return_value=mock_user): + result = protected_view() + assert result == "Protected content" + + +class TestGetUser: + """Test cases for _get_user function.""" + + def test_get_user_returns_user_from_g(self, app: Flask): + """Test that _get_user returns user from g._login_user.""" + mock_user = MockUser("test_user") + + with app.test_request_context(): + g._login_user = mock_user + user = _get_user() + assert user == mock_user + assert user.id == "test_user" + + def test_get_user_loads_user_if_not_in_g(self, app: Flask): + """Test that _get_user loads user if not already in g.""" + mock_user = MockUser("test_user") + + # Mock login manager + login_manager = MagicMock() + login_manager._load_user = MagicMock() + app.login_manager = login_manager + + with app.test_request_context(): + # Simulate _load_user setting g._login_user + def side_effect(): + g._login_user = mock_user + + login_manager._load_user.side_effect = side_effect + + user = _get_user() + assert user == mock_user + login_manager._load_user.assert_called_once() + + def test_get_user_returns_none_without_request_context(self, app: Flask): + """Test that _get_user returns None outside request context.""" + # Outside of request context + user = _get_user() + assert user is None + + +class TestCurrentUser: + """Test cases for current_user proxy.""" + + def test_current_user_proxy_returns_authenticated_user(self, app: Flask): + """Test that current_user proxy returns authenticated user.""" + mock_user = MockUser("test_user", is_authenticated=True) + + with app.test_request_context(): + with patch("libs.login._get_user", return_value=mock_user): + assert current_user.id == "test_user" + assert current_user.is_authenticated is True + + def test_current_user_proxy_returns_none_when_no_user(self, app: Flask): + """Test that current_user proxy handles None user.""" + with app.test_request_context(): + with patch("libs.login._get_user", return_value=None): + # When _get_user returns None, accessing attributes should fail + # or current_user should evaluate to falsy + try: + # Try to access an attribute that would exist on a real user + _ = current_user.id + pytest.fail("Should have raised AttributeError") + except AttributeError: + # This is expected when current_user is None + pass + + def test_current_user_proxy_thread_safety(self, app: Flask): + """Test that current_user proxy is thread-safe.""" + import threading + + results = {} + + def check_user_in_thread(user_id: str, index: int): + with app.test_request_context(): + mock_user = MockUser(user_id) + with patch("libs.login._get_user", return_value=mock_user): + results[index] = current_user.id + + # Create multiple threads with different users + threads = [] + for i in range(5): + thread = threading.Thread(target=check_user_in_thread, args=(f"user_{i}", i)) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join() + + # Verify each thread got its own user + for i in range(5): + assert results[i] == f"user_{i}" diff --git a/api/tests/unit_tests/libs/test_passport.py b/api/tests/unit_tests/libs/test_passport.py new file mode 100644 index 0000000000..f33484c18d --- /dev/null +++ b/api/tests/unit_tests/libs/test_passport.py @@ -0,0 +1,205 @@ +from datetime import UTC, datetime, timedelta +from unittest.mock import patch + +import jwt +import pytest +from werkzeug.exceptions import Unauthorized + +from libs.passport import PassportService + + +class TestPassportService: + """Test PassportService JWT operations""" + + @pytest.fixture + def passport_service(self): + """Create PassportService instance with test secret key""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + return PassportService() + + @pytest.fixture + def another_passport_service(self): + """Create another PassportService instance with different secret key""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "another-secret-key-for-testing" + return PassportService() + + # Core functionality tests + def test_should_issue_and_verify_token(self, passport_service): + """Test complete JWT lifecycle: issue and verify""" + payload = {"user_id": "123", "app_code": "test-app"} + token = passport_service.issue(payload) + + # Verify token format + assert isinstance(token, str) + assert len(token.split(".")) == 3 # JWT format: header.payload.signature + + # Verify token content + decoded = passport_service.verify(token) + assert decoded == payload + + def test_should_handle_different_payload_types(self, passport_service): + """Test issuing and verifying tokens with different payload types""" + test_cases = [ + {"string": "value"}, + {"number": 42}, + {"float": 3.14}, + {"boolean": True}, + {"null": None}, + {"array": [1, 2, 3]}, + {"nested": {"key": "value"}}, + {"unicode": "中文测试"}, + {"emoji": "🔐"}, + {}, # Empty payload + ] + + for payload in test_cases: + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + assert decoded == payload + + # Security tests + def test_should_reject_modified_token(self, passport_service): + """Test that any modification to token invalidates it""" + token = passport_service.issue({"user": "test"}) + + # Test multiple modification points + test_positions = [0, len(token) // 3, len(token) // 2, len(token) - 1] + + for pos in test_positions: + if pos < len(token) and token[pos] != ".": + # Change one character + tampered = token[:pos] + ("X" if token[pos] != "X" else "Y") + token[pos + 1 :] + with pytest.raises(Unauthorized): + passport_service.verify(tampered) + + def test_should_reject_token_with_different_secret_key(self, passport_service, another_passport_service): + """Test key isolation - token from one service should not work with another""" + payload = {"user_id": "123", "app_code": "test-app"} + token = passport_service.issue(payload) + + with pytest.raises(Unauthorized) as exc_info: + another_passport_service.verify(token) + assert str(exc_info.value) == "401 Unauthorized: Invalid token signature." + + def test_should_use_hs256_algorithm(self, passport_service): + """Test that HS256 algorithm is used for signing""" + payload = {"test": "data"} + token = passport_service.issue(payload) + + # Decode header without relying on JWT internals + # Use jwt.get_unverified_header which is a public API + header = jwt.get_unverified_header(token) + assert header["alg"] == "HS256" + + def test_should_reject_token_with_wrong_algorithm(self, passport_service): + """Test rejection of token signed with different algorithm""" + payload = {"user_id": "123"} + + # Create token with different algorithm + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + # Create token with HS512 instead of HS256 + wrong_alg_token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS512") + + # Should fail because service expects HS256 + # InvalidAlgorithmError is now caught by PyJWTError handler + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(wrong_alg_token) + assert str(exc_info.value) == "401 Unauthorized: Invalid token." + + # Exception handling tests + def test_should_handle_invalid_tokens(self, passport_service): + """Test handling of various invalid token formats""" + invalid_tokens = [ + ("not.a.token", "Invalid token."), + ("invalid-jwt-format", "Invalid token."), + ("xxx.yyy.zzz", "Invalid token."), + ("a.b", "Invalid token."), # Missing signature + ("", "Invalid token."), # Empty string + (" ", "Invalid token."), # Whitespace + (None, "Invalid token."), # None value + # Malformed base64 + ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.INVALID_BASE64!@#$.signature", "Invalid token."), + ] + + for invalid_token, expected_message in invalid_tokens: + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(invalid_token) + assert expected_message in str(exc_info.value) + + def test_should_reject_expired_token(self, passport_service): + """Test rejection of expired token""" + past_time = datetime.now(UTC) - timedelta(hours=1) + payload = {"user_id": "123", "exp": past_time.timestamp()} + + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "test-secret-key-for-testing" + token = jwt.encode(payload, mock_config.SECRET_KEY, algorithm="HS256") + + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify(token) + assert str(exc_info.value) == "401 Unauthorized: Token has expired." + + # Configuration tests + def test_should_handle_empty_secret_key(self): + """Test behavior when SECRET_KEY is empty""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = "" + service = PassportService() + + # Empty secret key should still work but is insecure + payload = {"test": "data"} + token = service.issue(payload) + decoded = service.verify(token) + assert decoded == payload + + def test_should_handle_none_secret_key(self): + """Test behavior when SECRET_KEY is None""" + with patch("libs.passport.dify_config") as mock_config: + mock_config.SECRET_KEY = None + service = PassportService() + + payload = {"test": "data"} + # JWT library will raise TypeError when secret is None + with pytest.raises((TypeError, jwt.exceptions.InvalidKeyError)): + service.issue(payload) + + # Boundary condition tests + def test_should_handle_large_payload(self, passport_service): + """Test handling of large payload""" + # Test with 100KB instead of 1MB for faster tests + large_data = "x" * (100 * 1024) + payload = {"data": large_data} + + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + + assert decoded["data"] == large_data + + def test_should_handle_special_characters_in_payload(self, passport_service): + """Test handling of special characters in payload""" + special_payloads = [ + {"special": "!@#$%^&*()"}, + {"quotes": 'He said "Hello"'}, + {"backslash": "path\\to\\file"}, + {"newline": "line1\nline2"}, + {"unicode": "🔐🔑🛡️"}, + {"mixed": "Test123!@#中文🔐"}, + ] + + for payload in special_payloads: + token = passport_service.issue(payload) + decoded = passport_service.verify(token) + assert decoded == payload + + def test_should_catch_generic_pyjwt_errors(self, passport_service): + """Test that generic PyJWTError exceptions are caught and converted to Unauthorized""" + # Mock jwt.decode to raise a generic PyJWTError + with patch("libs.passport.jwt.decode") as mock_decode: + mock_decode.side_effect = jwt.exceptions.PyJWTError("Generic JWT error") + + with pytest.raises(Unauthorized) as exc_info: + passport_service.verify("some-token") + assert str(exc_info.value) == "401 Unauthorized: Invalid token." diff --git a/api/tests/unit_tests/libs/test_password.py b/api/tests/unit_tests/libs/test_password.py new file mode 100644 index 0000000000..79fc792cc5 --- /dev/null +++ b/api/tests/unit_tests/libs/test_password.py @@ -0,0 +1,74 @@ +import base64 +import binascii +import os + +import pytest + +from libs.password import compare_password, hash_password, valid_password + + +class TestValidPassword: + """Test password format validation""" + + def test_should_accept_valid_passwords(self): + """Test accepting valid password formats""" + assert valid_password("password123") == "password123" + assert valid_password("test1234") == "test1234" + assert valid_password("Test123456") == "Test123456" + + def test_should_reject_invalid_passwords(self): + """Test rejecting invalid password formats""" + # Too short + with pytest.raises(ValueError) as exc_info: + valid_password("abc123") + assert "Password must contain letters and numbers" in str(exc_info.value) + + # No numbers + with pytest.raises(ValueError): + valid_password("abcdefgh") + + # No letters + with pytest.raises(ValueError): + valid_password("12345678") + + # Empty + with pytest.raises(ValueError): + valid_password("") + + +class TestPasswordHashing: + """Test password hashing and comparison""" + + def setup_method(self): + """Setup test data""" + self.password = "test123password" + self.salt = os.urandom(16) + self.salt_base64 = base64.b64encode(self.salt).decode() + + password_hash = hash_password(self.password, self.salt) + self.password_hash_base64 = base64.b64encode(password_hash).decode() + + def test_should_verify_correct_password(self): + """Test correct password verification""" + result = compare_password(self.password, self.password_hash_base64, self.salt_base64) + assert result is True + + def test_should_reject_wrong_password(self): + """Test rejection of incorrect passwords""" + result = compare_password("wrongpassword", self.password_hash_base64, self.salt_base64) + assert result is False + + def test_should_handle_invalid_base64(self): + """Test handling of invalid base64 data""" + # Invalid base64 hash + with pytest.raises(binascii.Error): + compare_password(self.password, "invalid_base64!", self.salt_base64) + + # Invalid base64 salt + with pytest.raises(binascii.Error): + compare_password(self.password, self.password_hash_base64, "invalid_base64!") + + def test_should_be_case_sensitive(self): + """Test password case sensitivity""" + result = compare_password(self.password.upper(), self.password_hash_base64, self.salt_base64) + assert result is False diff --git a/api/tests/unit_tests/libs/test_uuid_utils.py b/api/tests/unit_tests/libs/test_uuid_utils.py new file mode 100644 index 0000000000..7dbda95f45 --- /dev/null +++ b/api/tests/unit_tests/libs/test_uuid_utils.py @@ -0,0 +1,351 @@ +import struct +import time +import uuid +from unittest import mock + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from libs.uuid_utils import _create_uuidv7_bytes, uuidv7, uuidv7_boundary, uuidv7_timestamp + + +# Tests for private helper function _create_uuidv7_bytes +def test_create_uuidv7_bytes_basic_structure(): + """Test basic byte structure creation.""" + timestamp_ms = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + random_bytes = b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x11\x22" + + result = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + # Should be exactly 16 bytes + assert len(result) == 16 + assert isinstance(result, bytes) + + # Create UUID from bytes to verify it's valid + uuid_obj = uuid.UUID(bytes=result) + assert uuid_obj.version == 7 + + +def test_create_uuidv7_bytes_timestamp_encoding(): + """Test timestamp is correctly encoded in first 48 bits.""" + timestamp_ms = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + random_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + result = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + # Extract timestamp from first 6 bytes + timestamp_bytes = b"\x00\x00" + result[0:6] + extracted_timestamp = struct.unpack(">Q", timestamp_bytes)[0] + + assert extracted_timestamp == timestamp_ms + + +def test_create_uuidv7_bytes_version_bits(): + """Test version bits are set to 7.""" + timestamp_ms = 1609459200000 + random_bytes = b"\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00" # Set first 2 bytes to all 1s + + result = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + # Extract version from bytes 6-7 + version_and_rand_a = struct.unpack(">H", result[6:8])[0] + version = (version_and_rand_a >> 12) & 0x0F + + assert version == 7 + + +def test_create_uuidv7_bytes_variant_bits(): + """Test variant bits are set correctly.""" + timestamp_ms = 1609459200000 + random_bytes = b"\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00" # Set byte 8 to all 1s + + result = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + # Check variant bits in byte 8 (should be 10xxxxxx) + variant_byte = result[8] + variant_bits = (variant_byte >> 6) & 0b11 + + assert variant_bits == 0b10 # Should be binary 10 + + +def test_create_uuidv7_bytes_random_data(): + """Test random bytes are placed correctly.""" + timestamp_ms = 1609459200000 + random_bytes = b"\x12\x34\x56\x78\x9a\xbc\xde\xf0\x11\x22" + + result = _create_uuidv7_bytes(timestamp_ms, random_bytes) + + # Check random data A (12 bits from bytes 6-7, excluding version) + version_and_rand_a = struct.unpack(">H", result[6:8])[0] + rand_a = version_and_rand_a & 0x0FFF + expected_rand_a = struct.unpack(">H", random_bytes[0:2])[0] & 0x0FFF + assert rand_a == expected_rand_a + + # Check random data B (bytes 8-15, with variant bits preserved) + # Byte 8 should have variant bits set but preserve lower 6 bits + expected_byte_8 = (random_bytes[2] & 0x3F) | 0x80 + assert result[8] == expected_byte_8 + + # Bytes 9-15 should match random_bytes[3:10] + assert result[9:16] == random_bytes[3:10] + + +def test_create_uuidv7_bytes_zero_random(): + """Test with zero random bytes (boundary case).""" + timestamp_ms = 1609459200000 + zero_random_bytes = b"\x00" * 10 + + result = _create_uuidv7_bytes(timestamp_ms, zero_random_bytes) + + # Should still be valid UUIDv7 + uuid_obj = uuid.UUID(bytes=result) + assert uuid_obj.version == 7 + + # Version bits should be 0x7000 + version_and_rand_a = struct.unpack(">H", result[6:8])[0] + assert version_and_rand_a == 0x7000 + + # Variant byte should be 0x80 (variant bits + zero random bits) + assert result[8] == 0x80 + + # Remaining bytes should be zero + assert result[9:16] == b"\x00" * 7 + + +def test_uuidv7_basic_generation(): + """Test basic UUID generation produces valid UUIDv7.""" + result = uuidv7() + + # Should be a UUID object + assert isinstance(result, uuid.UUID) + + # Should be version 7 + assert result.version == 7 + + # Should have correct variant (RFC 4122 variant) + # Variant bits should be 10xxxxxx (0x80-0xBF range) + variant_byte = result.bytes[8] + assert (variant_byte >> 6) == 0b10 + + +def test_uuidv7_with_custom_timestamp(): + """Test UUID generation with custom timestamp.""" + custom_timestamp = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + result = uuidv7(custom_timestamp) + + assert isinstance(result, uuid.UUID) + assert result.version == 7 + + # Extract and verify timestamp + extracted_timestamp = uuidv7_timestamp(result) + assert isinstance(extracted_timestamp, int) + assert extracted_timestamp == custom_timestamp # Exact match for integer milliseconds + + +def test_uuidv7_with_none_timestamp(monkeypatch): + """Test UUID generation with None timestamp uses current time.""" + mock_time = 1609459200 + mock_time_func = mock.Mock(return_value=mock_time) + monkeypatch.setattr("time.time", mock_time_func) + result = uuidv7(None) + + assert isinstance(result, uuid.UUID) + assert result.version == 7 + + # Should use the mocked current time (converted to milliseconds) + assert mock_time_func.called + extracted_timestamp = uuidv7_timestamp(result) + assert extracted_timestamp == mock_time * 1000 # 1609459200.0 * 1000 + + +def test_uuidv7_time_ordering(): + """Test that sequential UUIDs have increasing timestamps.""" + # Generate UUIDs with incrementing timestamps (in milliseconds) + timestamp1 = 1609459200000 # 2021-01-01 00:00:00 UTC + timestamp2 = 1609459201000 # 2021-01-01 00:00:01 UTC + timestamp3 = 1609459202000 # 2021-01-01 00:00:02 UTC + + uuid1 = uuidv7(timestamp1) + uuid2 = uuidv7(timestamp2) + uuid3 = uuidv7(timestamp3) + + # Extract timestamps + ts1 = uuidv7_timestamp(uuid1) + ts2 = uuidv7_timestamp(uuid2) + ts3 = uuidv7_timestamp(uuid3) + + # Should be in ascending order + assert ts1 < ts2 < ts3 + + # UUIDs should be lexicographically ordered by their string representation + # due to time-ordering property of UUIDv7 + uuid_strings = [str(uuid1), str(uuid2), str(uuid3)] + assert uuid_strings == sorted(uuid_strings) + + +def test_uuidv7_uniqueness(): + """Test that multiple calls generate different UUIDs.""" + # Generate multiple UUIDs with the same timestamp (in milliseconds) + timestamp = 1609459200000 + uuids = [uuidv7(timestamp) for _ in range(100)] + + # All should be unique despite same timestamp (due to random bits) + assert len(set(uuids)) == 100 + + # All should have the same extracted timestamp + for uuid_obj in uuids: + extracted_ts = uuidv7_timestamp(uuid_obj) + assert extracted_ts == timestamp + + +def test_uuidv7_timestamp_error_handling_wrong_version(): + """Test error handling for non-UUIDv7 inputs.""" + + uuid_v4 = uuid.uuid4() + with pytest.raises(ValueError) as exc_ctx: + uuidv7_timestamp(uuid_v4) + assert "Expected UUIDv7 (version 7)" in str(exc_ctx.value) + assert f"got version {uuid_v4.version}" in str(exc_ctx.value) + + +@given(st.integers(max_value=2**48 - 1, min_value=0)) +def test_uuidv7_timestamp_round_trip(timestamp_ms): + # Generate UUID with timestamp + uuid_obj = uuidv7(timestamp_ms) + + # Extract timestamp back + extracted_timestamp = uuidv7_timestamp(uuid_obj) + + # Should match exactly for integer millisecond timestamps + assert extracted_timestamp == timestamp_ms + + +def test_uuidv7_timestamp_edge_cases(): + """Test timestamp extraction with edge case values.""" + # Test with very small timestamp + small_timestamp = 1 # 1ms after epoch + uuid_small = uuidv7(small_timestamp) + extracted_small = uuidv7_timestamp(uuid_small) + assert extracted_small == small_timestamp + + # Test with large timestamp (year 2038+) + large_timestamp = 2147483647000 # 2038-01-19 03:14:07 UTC in milliseconds + uuid_large = uuidv7(large_timestamp) + extracted_large = uuidv7_timestamp(uuid_large) + assert extracted_large == large_timestamp + + +def test_uuidv7_boundary_basic_generation(): + """Test basic boundary UUID generation with a known timestamp.""" + timestamp = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + result = uuidv7_boundary(timestamp) + + # Should be a UUID object + assert isinstance(result, uuid.UUID) + + # Should be version 7 + assert result.version == 7 + + # Should have correct variant (RFC 4122 variant) + # Variant bits should be 10xxxxxx (0x80-0xBF range) + variant_byte = result.bytes[8] + assert (variant_byte >> 6) == 0b10 + + +def test_uuidv7_boundary_timestamp_extraction(): + """Test that boundary UUID timestamp can be extracted correctly.""" + timestamp = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + boundary_uuid = uuidv7_boundary(timestamp) + + # Extract timestamp using existing function + extracted_timestamp = uuidv7_timestamp(boundary_uuid) + + # Should match exactly + assert extracted_timestamp == timestamp + + +def test_uuidv7_boundary_deterministic(): + """Test that boundary UUIDs are deterministic for same timestamp.""" + timestamp = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + + # Generate multiple boundary UUIDs with same timestamp + uuid1 = uuidv7_boundary(timestamp) + uuid2 = uuidv7_boundary(timestamp) + uuid3 = uuidv7_boundary(timestamp) + + # Should all be identical + assert uuid1 == uuid2 == uuid3 + assert str(uuid1) == str(uuid2) == str(uuid3) + + +def test_uuidv7_boundary_is_minimum(): + """Test that boundary UUID is lexicographically smaller than regular UUIDs.""" + timestamp = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds + + # Generate boundary UUID + boundary_uuid = uuidv7_boundary(timestamp) + + # Generate multiple regular UUIDs with same timestamp + regular_uuids = [uuidv7(timestamp) for _ in range(50)] + + # Boundary UUID should be lexicographically smaller than all regular UUIDs + boundary_str = str(boundary_uuid) + for regular_uuid in regular_uuids: + regular_str = str(regular_uuid) + assert boundary_str < regular_str, f"Boundary {boundary_str} should be < regular {regular_str}" + + # Also test with bytes comparison + boundary_bytes = boundary_uuid.bytes + for regular_uuid in regular_uuids: + regular_bytes = regular_uuid.bytes + assert boundary_bytes < regular_bytes + + +def test_uuidv7_boundary_different_timestamps(): + """Test that boundary UUIDs with different timestamps are ordered correctly.""" + timestamp1 = 1609459200000 # 2021-01-01 00:00:00 UTC + timestamp2 = 1609459201000 # 2021-01-01 00:00:01 UTC + timestamp3 = 1609459202000 # 2021-01-01 00:00:02 UTC + + uuid1 = uuidv7_boundary(timestamp1) + uuid2 = uuidv7_boundary(timestamp2) + uuid3 = uuidv7_boundary(timestamp3) + + # Extract timestamps to verify + ts1 = uuidv7_timestamp(uuid1) + ts2 = uuidv7_timestamp(uuid2) + ts3 = uuidv7_timestamp(uuid3) + + # Should be in ascending order + assert ts1 < ts2 < ts3 + + # UUIDs should be lexicographically ordered + uuid_strings = [str(uuid1), str(uuid2), str(uuid3)] + assert uuid_strings == sorted(uuid_strings) + + # Bytes should also be ordered + assert uuid1.bytes < uuid2.bytes < uuid3.bytes + + +def test_uuidv7_boundary_edge_cases(): + """Test boundary UUID generation with edge case timestamp values.""" + # Test with timestamp 0 (Unix epoch) + epoch_uuid = uuidv7_boundary(0) + assert isinstance(epoch_uuid, uuid.UUID) + assert epoch_uuid.version == 7 + assert uuidv7_timestamp(epoch_uuid) == 0 + + # Test with very large timestamp values + large_timestamp = 2147483647000 # 2038-01-19 03:14:07 UTC in milliseconds + large_uuid = uuidv7_boundary(large_timestamp) + assert isinstance(large_uuid, uuid.UUID) + assert large_uuid.version == 7 + assert uuidv7_timestamp(large_uuid) == large_timestamp + + # Test with current time + current_time = int(time.time() * 1000) + current_uuid = uuidv7_boundary(current_time) + assert isinstance(current_uuid, uuid.UUID) + assert current_uuid.version == 7 + assert uuidv7_timestamp(current_uuid) == current_time diff --git a/api/tests/unit_tests/models/__init__.py b/api/tests/unit_tests/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/models/test_workflow.py b/api/tests/unit_tests/models/test_workflow.py index b79e95c7ed..5bc77ad0ef 100644 --- a/api/tests/unit_tests/models/test_workflow.py +++ b/api/tests/unit_tests/models/test_workflow.py @@ -1,10 +1,16 @@ +import dataclasses import json from unittest import mock from uuid import uuid4 from constants import HIDDEN_VALUE +from core.file.enums import FileTransferMethod, FileType +from core.file.models import File from core.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable -from models.workflow import Workflow, WorkflowNodeExecutionModel +from core.variables.segments import IntegerSegment, Segment +from factories.variable_factory import build_segment +from models.model import EndUser +from models.workflow import Workflow, WorkflowDraftVariable, WorkflowNodeExecutionModel, is_system_variable_editable def test_environment_variables(): @@ -38,7 +44,7 @@ def test_environment_variables(): ) # Mock current_user as an EndUser - mock_user = mock.Mock() + mock_user = mock.Mock(spec=EndUser) mock_user.tenant_id = "tenant_id" with ( @@ -85,7 +91,7 @@ def test_update_environment_variables(): ) # Mock current_user as an EndUser - mock_user = mock.Mock() + mock_user = mock.Mock(spec=EndUser) mock_user.tenant_id = "tenant_id" with ( @@ -131,7 +137,7 @@ def test_to_dict(): # Create some EnvironmentVariable instances # Mock current_user as an EndUser - mock_user = mock.Mock() + mock_user = mock.Mock(spec=EndUser) mock_user.tenant_id = "tenant_id" with ( @@ -163,3 +169,147 @@ class TestWorkflowNodeExecution: original = {"a": 1, "b": ["2"]} node_exec.execution_metadata = json.dumps(original) assert node_exec.execution_metadata_dict == original + + +class TestIsSystemVariableEditable: + def test_is_system_variable(self): + cases = [ + ("query", True), + ("files", True), + ("dialogue_count", False), + ("conversation_id", False), + ("user_id", False), + ("app_id", False), + ("workflow_id", False), + ("workflow_run_id", False), + ] + for name, editable in cases: + assert editable == is_system_variable_editable(name) + + assert is_system_variable_editable("invalid_or_new_system_variable") == False + + +class TestWorkflowDraftVariableGetValue: + def test_get_value_by_case(self): + @dataclasses.dataclass + class TestCase: + name: str + value: Segment + + tenant_id = "test_tenant_id" + + test_file = File( + tenant_id=tenant_id, + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://example.com/example.jpg", + filename="example.jpg", + extension=".jpg", + mime_type="image/jpeg", + size=100, + ) + cases: list[TestCase] = [ + TestCase( + name="number/int", + value=build_segment(1), + ), + TestCase( + name="number/float", + value=build_segment(1.0), + ), + TestCase( + name="string", + value=build_segment("a"), + ), + TestCase( + name="object", + value=build_segment({}), + ), + TestCase( + name="file", + value=build_segment(test_file), + ), + TestCase( + name="array[any]", + value=build_segment([1, "a"]), + ), + TestCase( + name="array[string]", + value=build_segment(["a", "b"]), + ), + TestCase( + name="array[number]/int", + value=build_segment([1, 2]), + ), + TestCase( + name="array[number]/float", + value=build_segment([1.0, 2.0]), + ), + TestCase( + name="array[number]/mixed", + value=build_segment([1, 2.0]), + ), + TestCase( + name="array[object]", + value=build_segment([{}, {"a": 1}]), + ), + TestCase( + name="none", + value=build_segment(None), + ), + ] + + for idx, c in enumerate(cases, 1): + fail_msg = f"test case {c.name} failed, index={idx}" + draft_var = WorkflowDraftVariable() + draft_var.set_value(c.value) + assert c.value == draft_var.get_value(), fail_msg + + def test_file_variable_preserves_all_fields(self): + """Test that File type variables preserve all fields during encoding/decoding.""" + tenant_id = "test_tenant_id" + + # Create a File with specific field values + test_file = File( + id="test_file_id", + tenant_id=tenant_id, + type=FileType.IMAGE, + transfer_method=FileTransferMethod.REMOTE_URL, + remote_url="https://example.com/test.jpg", + filename="test.jpg", + extension=".jpg", + mime_type="image/jpeg", + size=12345, # Specific size to test preservation + storage_key="test_storage_key", + ) + + # Create a FileSegment and WorkflowDraftVariable + file_segment = build_segment(test_file) + draft_var = WorkflowDraftVariable() + draft_var.set_value(file_segment) + + # Retrieve the value and verify all fields are preserved + retrieved_segment = draft_var.get_value() + retrieved_file = retrieved_segment.value + + # Verify all important fields are preserved + assert retrieved_file.id == test_file.id + assert retrieved_file.tenant_id == test_file.tenant_id + assert retrieved_file.type == test_file.type + assert retrieved_file.transfer_method == test_file.transfer_method + assert retrieved_file.remote_url == test_file.remote_url + assert retrieved_file.filename == test_file.filename + assert retrieved_file.extension == test_file.extension + assert retrieved_file.mime_type == test_file.mime_type + assert retrieved_file.size == test_file.size # This was the main issue being fixed + # Note: storage_key is not serialized in model_dump() so it won't be preserved + + # Verify the segments have the same type and the important fields match + assert file_segment.value_type == retrieved_segment.value_type + + def test_get_and_set_value(self): + draft_var = WorkflowDraftVariable() + int_var = IntegerSegment(value=1) + draft_var.set_value(int_var) + value = draft_var.get_value() + assert value == int_var diff --git a/api/tests/unit_tests/services/services_test_help.py b/api/tests/unit_tests/services/services_test_help.py new file mode 100644 index 0000000000..c6b962f7fc --- /dev/null +++ b/api/tests/unit_tests/services/services_test_help.py @@ -0,0 +1,59 @@ +from unittest.mock import MagicMock + + +class ServiceDbTestHelper: + """ + Helper class for service database query tests. + """ + + @staticmethod + def setup_db_query_filter_by_mock(mock_db, query_results): + """ + Smart database query mock that responds based on model type and query parameters. + + Args: + mock_db: Mock database session + query_results: Dict mapping (model_name, filter_key, filter_value) to return value + Example: {('Account', 'email', 'test@example.com'): mock_account} + """ + + def query_side_effect(model): + mock_query = MagicMock() + + def filter_by_side_effect(**kwargs): + mock_filter_result = MagicMock() + + def first_side_effect(): + # Find matching result based on model and filter parameters + for (model_name, filter_key, filter_value), result in query_results.items(): + if model.__name__ == model_name and filter_key in kwargs and kwargs[filter_key] == filter_value: + return result + return None + + mock_filter_result.first.side_effect = first_side_effect + + # Handle order_by calls for complex queries + def order_by_side_effect(*args, **kwargs): + mock_order_result = MagicMock() + + def order_first_side_effect(): + # Look for order_by results in the same query_results dict + for (model_name, filter_key, filter_value), result in query_results.items(): + if ( + model.__name__ == model_name + and filter_key == "order_by" + and filter_value == "first_available" + ): + return result + return None + + mock_order_result.first.side_effect = order_first_side_effect + return mock_order_result + + mock_filter_result.order_by.side_effect = order_by_side_effect + return mock_filter_result + + mock_query.filter_by.side_effect = filter_by_side_effect + return mock_query + + mock_db.session.query.side_effect = query_side_effect diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py new file mode 100644 index 0000000000..13900ab6d1 --- /dev/null +++ b/api/tests/unit_tests/services/test_account_service.py @@ -0,0 +1,1545 @@ +import json +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +import pytest + +from configs import dify_config +from models.account import Account +from services.account_service import AccountService, RegisterService, TenantService +from services.errors.account import ( + AccountAlreadyInTenantError, + AccountLoginError, + AccountNotFoundError, + AccountPasswordError, + AccountRegisterError, + CurrentPasswordIncorrectError, +) +from tests.unit_tests.services.services_test_help import ServiceDbTestHelper + + +class TestAccountAssociatedDataFactory: + """Factory class for creating test data and mock objects for account service tests.""" + + @staticmethod + def create_account_mock( + account_id: str = "user-123", + email: str = "test@example.com", + name: str = "Test User", + status: str = "active", + password: str = "hashed_password", + password_salt: str = "salt", + interface_language: str = "en-US", + interface_theme: str = "light", + timezone: str = "UTC", + **kwargs, + ) -> MagicMock: + """Create a mock account with specified attributes.""" + account = MagicMock(spec=Account) + account.id = account_id + account.email = email + account.name = name + account.status = status + account.password = password + account.password_salt = password_salt + account.interface_language = interface_language + account.interface_theme = interface_theme + account.timezone = timezone + # Set last_active_at to a datetime object that's older than 10 minutes + account.last_active_at = datetime.now() - timedelta(minutes=15) + account.initialized_at = None + for key, value in kwargs.items(): + setattr(account, key, value) + return account + + @staticmethod + def create_tenant_join_mock( + tenant_id: str = "tenant-456", + account_id: str = "user-123", + current: bool = True, + role: str = "normal", + **kwargs, + ) -> MagicMock: + """Create a mock tenant account join record.""" + tenant_join = MagicMock() + tenant_join.tenant_id = tenant_id + tenant_join.account_id = account_id + tenant_join.current = current + tenant_join.role = role + for key, value in kwargs.items(): + setattr(tenant_join, key, value) + return tenant_join + + @staticmethod + def create_feature_service_mock(allow_register: bool = True): + """Create a mock feature service.""" + mock_service = MagicMock() + mock_service.get_system_features.return_value.is_allow_register = allow_register + return mock_service + + @staticmethod + def create_billing_service_mock(email_frozen: bool = False): + """Create a mock billing service.""" + mock_service = MagicMock() + mock_service.is_email_in_freeze.return_value = email_frozen + return mock_service + + +class TestAccountService: + """ + Comprehensive unit tests for AccountService methods. + + This test suite covers all account-related operations including: + - Authentication and login + - Account creation and registration + - Password management + - JWT token generation + - User loading and tenant management + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_password_dependencies(self): + """Mock setup for password-related functions.""" + with ( + patch("services.account_service.compare_password") as mock_compare_password, + patch("services.account_service.hash_password") as mock_hash_password, + patch("services.account_service.valid_password") as mock_valid_password, + ): + yield { + "compare_password": mock_compare_password, + "hash_password": mock_hash_password, + "valid_password": mock_valid_password, + } + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + patch("services.account_service.PassportService") as mock_passport_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + "passport_service": mock_passport_service, + } + + @pytest.fixture + def mock_db_with_autospec(self): + """ + Mock database with autospec for more realistic behavior. + This approach preserves the actual method signatures and behavior. + """ + with patch("services.account_service.db", autospec=True) as mock_db: + # Create a more realistic session mock + mock_session = MagicMock() + mock_db.session = mock_session + + # Setup basic session methods + mock_session.add = MagicMock() + mock_session.commit = MagicMock() + mock_session.query = MagicMock() + + yield mock_db + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_database_operations_not_called(self, mock_db): + """Helper method to verify database operations were not called.""" + mock_db.session.commit.assert_not_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Authentication Tests ==================== + + def test_authenticate_success(self, mock_db_dependencies, mock_password_dependencies): + """Test successful authentication with correct email and password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock + query_results = {("Account", "email", "test@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = True + + # Execute test + result = AccountService.authenticate("test@example.com", "password") + + # Verify results + assert result == mock_account + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_authenticate_account_not_found(self, mock_db_dependencies): + """Test authentication when account does not exist.""" + # Setup smart database query mock - no matching results + query_results = {("Account", "email", "notfound@example.com"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised( + AccountNotFoundError, AccountService.authenticate, "notfound@example.com", "password" + ) + + def test_authenticate_account_banned(self, mock_db_dependencies): + """Test authentication when account is banned.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") + + # Setup smart database query mock + query_results = {("Account", "email", "banned@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "banned@example.com", "password") + + def test_authenticate_password_error(self, mock_db_dependencies, mock_password_dependencies): + """Test authentication with wrong password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock + query_results = {("Account", "email", "test@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = False + + # Execute test and verify exception + self._assert_exception_raised( + AccountPasswordError, AccountService.authenticate, "test@example.com", "wrongpassword" + ) + + def test_authenticate_pending_account_activates(self, mock_db_dependencies, mock_password_dependencies): + """Test authentication for a pending account, which should activate on login.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending") + + # Setup smart database query mock + query_results = {("Account", "email", "pending@example.com"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + mock_password_dependencies["compare_password"].return_value = True + + # Execute test + result = AccountService.authenticate("pending@example.com", "password") + + # Verify results + assert result == mock_account + assert mock_account.status == "active" + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Account Creation Tests ==================== + + def test_create_account_success( + self, mock_db_dependencies, mock_password_dependencies, mock_external_service_dependencies + ): + """Test successful account creation with all required parameters.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + mock_password_dependencies["hash_password"].return_value = b"hashed_password" + + # Execute test + result = AccountService.create_account( + email="test@example.com", + name="Test User", + interface_language="en-US", + password="password123", + interface_theme="light", + ) + + # Verify results + assert result.email == "test@example.com" + assert result.name == "Test User" + assert result.interface_language == "en-US" + assert result.interface_theme == "light" + assert result.password is not None + assert result.password_salt is not None + assert result.timezone is not None + + # Verify database operations + mock_db_dependencies["db"].session.add.assert_called_once() + added_account = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_account.email == "test@example.com" + assert added_account.name == "Test User" + assert added_account.interface_language == "en-US" + assert added_account.interface_theme == "light" + assert added_account.password is not None + assert added_account.password_salt is not None + assert added_account.timezone is not None + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_create_account_registration_disabled(self, mock_external_service_dependencies): + """Test account creation when registration is disabled.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = False + + # Execute test and verify exception + self._assert_exception_raised( + Exception, # AccountNotFound + AccountService.create_account, + email="test@example.com", + name="Test User", + interface_language="en-US", + ) + + def test_create_account_email_frozen(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account creation with frozen email address.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = True + dify_config.BILLING_ENABLED = True + + # Execute test and verify exception + self._assert_exception_raised( + AccountRegisterError, + AccountService.create_account, + email="frozen@example.com", + name="Test User", + interface_language="en-US", + ) + dify_config.BILLING_ENABLED = False + + def test_create_account_without_password(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account creation without password (for invite-based registration).""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Execute test + result = AccountService.create_account( + email="test@example.com", + name="Test User", + interface_language="zh-CN", + password=None, + interface_theme="dark", + ) + + # Verify results + assert result.email == "test@example.com" + assert result.name == "Test User" + assert result.interface_language == "zh-CN" + assert result.interface_theme == "dark" + assert result.password is None + assert result.password_salt is None + assert result.timezone is not None + + # Verify database operations + mock_db_dependencies["db"].session.add.assert_called_once() + added_account = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_account.email == "test@example.com" + assert added_account.name == "Test User" + assert added_account.interface_language == "zh-CN" + assert added_account.interface_theme == "dark" + assert added_account.password is None + assert added_account.password_salt is None + assert added_account.timezone is not None + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Password Management Tests ==================== + + def test_update_account_password_success(self, mock_db_dependencies, mock_password_dependencies): + """Test successful password update with correct current password and valid new password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = True + mock_password_dependencies["valid_password"].return_value = None + mock_password_dependencies["hash_password"].return_value = b"new_hashed_password" + + # Execute test + result = AccountService.update_account_password(mock_account, "old_password", "new_password123") + + # Verify results + assert result == mock_account + assert mock_account.password is not None + assert mock_account.password_salt is not None + + # Verify password validation was called + mock_password_dependencies["compare_password"].assert_called_once_with( + "old_password", "hashed_password", "salt" + ) + mock_password_dependencies["valid_password"].assert_called_once_with("new_password123") + + # Verify database operations + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_update_account_password_current_password_incorrect(self, mock_password_dependencies): + """Test password update with incorrect current password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = False + + # Execute test and verify exception + self._assert_exception_raised( + CurrentPasswordIncorrectError, + AccountService.update_account_password, + mock_account, + "wrong_password", + "new_password123", + ) + + # Verify password comparison was called + mock_password_dependencies["compare_password"].assert_called_once_with( + "wrong_password", "hashed_password", "salt" + ) + + def test_update_account_password_invalid_new_password(self, mock_password_dependencies): + """Test password update with invalid new password.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_password_dependencies["compare_password"].return_value = True + mock_password_dependencies["valid_password"].side_effect = ValueError("Password too short") + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, AccountService.update_account_password, mock_account, "old_password", "short" + ) + + # Verify password validation was called + mock_password_dependencies["valid_password"].assert_called_once_with("short") + + # ==================== User Loading Tests ==================== + + def test_load_user_success(self, mock_db_dependencies): + """Test successful user loading with current tenant.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock() + + # Setup smart database query mock + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join, + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result == mock_account + assert mock_account.set_tenant_id.called + + def test_load_user_not_found(self, mock_db_dependencies): + """Test user loading when user does not exist.""" + # Setup smart database query mock - no matching results + query_results = {("Account", "id", "non-existent-user"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test + result = AccountService.load_user("non-existent-user") + + # Verify results + assert result is None + + def test_load_user_banned(self, mock_db_dependencies): + """Test user loading when user is banned.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned") + + # Setup smart database query mock + query_results = {("Account", "id", "user-123"): mock_account} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test and verify exception + self._assert_exception_raised( + Exception, # Unauthorized + AccountService.load_user, + "user-123", + ) + + def test_load_user_no_current_tenant(self, mock_db_dependencies): + """Test user loading when user has no current tenant but has available tenants.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False) + + # Setup smart database query mock for complex scenario + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant + ("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result == mock_account + assert mock_available_tenant.current is True + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_load_user_no_tenants(self, mock_db_dependencies): + """Test user loading when user has no tenants at all.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock for no tenants scenario + query_results = { + ("Account", "id", "user-123"): mock_account, + ("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant + ("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock datetime + with patch("services.account_service.datetime") as mock_datetime: + mock_now = datetime.now() + mock_datetime.now.return_value = mock_now + mock_datetime.UTC = "UTC" + + # Execute test + result = AccountService.load_user("user-123") + + # Verify results + assert result is None + + +class TestTenantService: + """ + Comprehensive unit tests for TenantService methods. + + This test suite covers all tenant-related operations including: + - Tenant creation and management + - Member management and permissions + - Tenant switching + - Role updates and permission checks + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_rsa_dependencies(self): + """Mock setup for RSA-related functions.""" + with patch("services.account_service.generate_key_pair") as mock_generate_key_pair: + yield mock_generate_key_pair + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + } + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Tenant Creation Tests ==================== + + def test_create_owner_tenant_if_not_exist_new_user( + self, mock_db_dependencies, mock_rsa_dependencies, mock_external_service_dependencies + ): + """Test creating owner tenant for new user without existing tenants.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock - no existing tenant joins + query_results = { + ("TenantAccountJoin", "account_id", "user-123"): None, + ("TenantAccountJoin", "tenant_id", "tenant-456"): None, # For has_roles check + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Setup external service mocks + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + + # Mock tenant creation + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test User's Workspace" + + # Mock database operations + mock_db_dependencies["db"].session.add = MagicMock() + + # Mock RSA key generation + mock_rsa_dependencies.return_value = "mock_public_key" + + # Mock has_roles method to return False (no existing owner) + with patch("services.account_service.TenantService.has_roles") as mock_has_roles: + mock_has_roles.return_value = False + + # Mock Tenant creation to set proper ID + with patch("services.account_service.Tenant") as mock_tenant_class: + mock_tenant_instance = MagicMock() + mock_tenant_instance.id = "tenant-456" + mock_tenant_instance.name = "Test User's Workspace" + mock_tenant_class.return_value = mock_tenant_instance + + # Execute test + TenantService.create_owner_tenant_if_not_exist(mock_account) + + # Verify tenant was created with correct parameters + mock_db_dependencies["db"].session.add.assert_called() + + # Get all calls to session.add + add_calls = mock_db_dependencies["db"].session.add.call_args_list + + # Should have at least 2 calls: one for Tenant, one for TenantAccountJoin + assert len(add_calls) >= 2 + + # Verify Tenant was added with correct name + tenant_added = False + tenant_account_join_added = False + + for call in add_calls: + added_object = call[0][0] # First argument of the call + + # Check if it's a Tenant object + if hasattr(added_object, "name") and hasattr(added_object, "id"): + # This should be a Tenant object + assert added_object.name == "Test User's Workspace" + tenant_added = True + + # Check if it's a TenantAccountJoin object + elif ( + hasattr(added_object, "tenant_id") + and hasattr(added_object, "account_id") + and hasattr(added_object, "role") + ): + # This should be a TenantAccountJoin object + assert added_object.tenant_id is not None + assert added_object.account_id == "user-123" + assert added_object.role == "owner" + tenant_account_join_added = True + + assert tenant_added, "Tenant object was not added to database" + assert tenant_account_join_added, "TenantAccountJoin object was not added to database" + + self._assert_database_operations_called(mock_db_dependencies["db"]) + assert mock_rsa_dependencies.called, "RSA key generation was not called" + + # ==================== Member Management Tests ==================== + + def test_create_tenant_member_success(self, mock_db_dependencies): + """Test successful tenant member creation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Setup smart database query mock - no existing member + query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): None} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock database operations + mock_db_dependencies["db"].session.add = MagicMock() + + # Execute test + result = TenantService.create_tenant_member(mock_tenant, mock_account, "normal") + + # Verify member was created with correct parameters + assert result is not None + mock_db_dependencies["db"].session.add.assert_called_once() + + # Verify the TenantAccountJoin object was added with correct parameters + added_tenant_account_join = mock_db_dependencies["db"].session.add.call_args[0][0] + assert added_tenant_account_join.tenant_id == "tenant-456" + assert added_tenant_account_join.account_id == "user-123" + assert added_tenant_account_join.role == "normal" + + self._assert_database_operations_called(mock_db_dependencies["db"]) + + # ==================== Tenant Switching Tests ==================== + + def test_switch_tenant_success(self): + """Test successful tenant switching.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="user-123", current=False + ) + + # Mock the complex query in switch_tenant method + with patch("services.account_service.db") as mock_db: + # Mock the join query that returns the tenant_account_join + mock_query = MagicMock() + mock_filter = MagicMock() + mock_filter.first.return_value = mock_tenant_join + mock_query.filter.return_value = mock_filter + mock_query.join.return_value = mock_query + mock_db.session.query.return_value = mock_query + + # Execute test + TenantService.switch_tenant(mock_account, "tenant-456") + + # Verify tenant was switched + assert mock_tenant_join.current is True + self._assert_database_operations_called(mock_db) + + def test_switch_tenant_no_tenant_id(self): + """Test tenant switching without providing tenant ID.""" + # Setup test data + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + + # Execute test and verify exception + self._assert_exception_raised(ValueError, TenantService.switch_tenant, mock_account, None) + + # ==================== Role Management Tests ==================== + + def test_update_member_role_success(self): + """Test successful member role update.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_member = TestAccountAssociatedDataFactory.create_account_mock(account_id="member-789") + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + mock_target_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="member-789", role="normal" + ) + mock_operator_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="operator-123", role="owner" + ) + + # Mock the database queries in update_member_role method + with patch("services.account_service.db") as mock_db: + # Mock the first query for operator permission check + mock_query1 = MagicMock() + mock_filter1 = MagicMock() + mock_filter1.first.return_value = mock_operator_join + mock_query1.filter_by.return_value = mock_filter1 + + # Mock the second query for target member + mock_query2 = MagicMock() + mock_filter2 = MagicMock() + mock_filter2.first.return_value = mock_target_join + mock_query2.filter_by.return_value = mock_filter2 + + # Make the query method return different mocks for different calls + mock_db.session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + TenantService.update_member_role(mock_tenant, mock_member, "admin", mock_operator) + + # Verify role was updated + assert mock_target_join.role == "admin" + self._assert_database_operations_called(mock_db) + + # ==================== Permission Check Tests ==================== + + def test_check_member_permission_success(self, mock_db_dependencies): + """Test successful member permission check.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + mock_member = TestAccountAssociatedDataFactory.create_account_mock(account_id="member-789") + mock_operator_join = TestAccountAssociatedDataFactory.create_tenant_join_mock( + tenant_id="tenant-456", account_id="operator-123", role="owner" + ) + + # Setup smart database query mock + query_results = {("TenantAccountJoin", "tenant_id", "tenant-456"): mock_operator_join} + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Execute test - should not raise exception + TenantService.check_member_permission(mock_tenant, mock_operator, mock_member, "add") + + def test_check_member_permission_operate_self(self): + """Test member permission check when operator tries to operate self.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_operator = TestAccountAssociatedDataFactory.create_account_mock(account_id="operator-123") + + # Execute test and verify exception + from services.errors.account import CannotOperateSelfError + + self._assert_exception_raised( + CannotOperateSelfError, + TenantService.check_member_permission, + mock_tenant, + mock_operator, + mock_operator, # Same as operator + "add", + ) + + +class TestRegisterService: + """ + Comprehensive unit tests for RegisterService methods. + + This test suite covers all registration-related operations including: + - System setup + - Account registration + - Member invitation + - Token management + - Invitation validation + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_db_dependencies(self): + """Common mock setup for database dependencies.""" + with patch("services.account_service.db") as mock_db: + mock_db.session.add = MagicMock() + mock_db.session.commit = MagicMock() + mock_db.session.begin_nested = MagicMock() + mock_db.session.rollback = MagicMock() + yield { + "db": mock_db, + } + + @pytest.fixture + def mock_redis_dependencies(self): + """Mock setup for Redis-related functions.""" + with patch("services.account_service.redis_client") as mock_redis: + yield mock_redis + + @pytest.fixture + def mock_external_service_dependencies(self): + """Mock setup for external service dependencies.""" + with ( + patch("services.account_service.FeatureService") as mock_feature_service, + patch("services.account_service.BillingService") as mock_billing_service, + patch("services.account_service.PassportService") as mock_passport_service, + ): + yield { + "feature_service": mock_feature_service, + "billing_service": mock_billing_service, + "passport_service": mock_passport_service, + } + + @pytest.fixture + def mock_task_dependencies(self): + """Mock setup for task dependencies.""" + with patch("services.account_service.send_invite_member_mail_task") as mock_send_mail: + yield mock_send_mail + + def _assert_database_operations_called(self, mock_db): + """Helper method to verify database operations were called.""" + mock_db.session.commit.assert_called() + + def _assert_database_operations_not_called(self, mock_db): + """Helper method to verify database operations were not called.""" + mock_db.session.commit.assert_not_called() + + def _assert_exception_raised(self, exception_type, callable_func, *args, **kwargs): + """Helper method to verify that specific exception is raised.""" + with pytest.raises(exception_type): + callable_func(*args, **kwargs) + + # ==================== Setup Tests ==================== + + def test_setup_success(self, mock_db_dependencies, mock_external_service_dependencies): + """Test successful system setup.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService.create_owner_tenant_if_not_exist + with patch("services.account_service.TenantService.create_owner_tenant_if_not_exist") as mock_create_tenant: + # Mock DifySetup + with patch("services.account_service.DifySetup") as mock_dify_setup: + mock_dify_setup_instance = MagicMock() + mock_dify_setup.return_value = mock_dify_setup_instance + + # Execute test + RegisterService.setup("admin@example.com", "Admin User", "password123", "192.168.1.1") + + # Verify results + mock_create_account.assert_called_once_with( + email="admin@example.com", + name="Admin User", + interface_language="en-US", + password="password123", + is_setup=True, + ) + mock_create_tenant.assert_called_once_with(account=mock_account, is_setup=True) + mock_dify_setup.assert_called_once() + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_setup_failure_rollback(self, mock_db_dependencies, mock_external_service_dependencies): + """Test setup failure with proper rollback.""" + # Setup mocks to simulate failure + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account to raise exception + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.side_effect = Exception("Database error") + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, + RegisterService.setup, + "admin@example.com", + "Admin User", + "password123", + "192.168.1.1", + ) + + # Verify rollback operations were called + mock_db_dependencies["db"].session.query.assert_called() + + # ==================== Registration Tests ==================== + + def test_register_success(self, mock_db_dependencies, mock_external_service_dependencies): + """Test successful account registration.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService.create_tenant and create_tenant_member + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_create_tenant.return_value = mock_tenant + + # Execute test + result = RegisterService.register( + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify results + assert result == mock_account + assert result.status == "active" + assert result.initialized_at is not None + mock_create_account.assert_called_once_with( + email="test@example.com", + name="Test User", + interface_language="en-US", + password="password123", + is_setup=False, + ) + mock_create_tenant.assert_called_once_with("Test User's Workspace") + mock_create_member.assert_called_once_with(mock_tenant, mock_account, role="owner") + mock_event.send.assert_called_once_with(mock_tenant) + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_with_oauth(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account registration with OAuth integration.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account and link_account_integrate + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with ( + patch("services.account_service.AccountService.create_account") as mock_create_account, + patch("services.account_service.AccountService.link_account_integrate") as mock_link_account, + ): + mock_create_account.return_value = mock_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_create_tenant.return_value = mock_tenant + + # Execute test + result = RegisterService.register( + email="test@example.com", + name="Test User", + password=None, + open_id="oauth123", + provider="google", + language="en-US", + ) + + # Verify results + assert result == mock_account + mock_link_account.assert_called_once_with("google", "oauth123", mock_account) + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_with_pending_status(self, mock_db_dependencies, mock_external_service_dependencies): + """Test account registration with pending status.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.create_tenant") as mock_create_tenant, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.tenant_was_created") as mock_event, + ): + mock_tenant = MagicMock() + mock_create_tenant.return_value = mock_tenant + + # Execute test with pending status + from models.account import AccountStatus + + result = RegisterService.register( + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + status=AccountStatus.PENDING, + ) + + # Verify results + assert result == mock_account + assert result.status == "pending" + self._assert_database_operations_called(mock_db_dependencies["db"]) + + def test_register_workspace_not_allowed(self, mock_db_dependencies, mock_external_service_dependencies): + """Test registration when workspace creation is not allowed.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.is_allow_create_workspace = True + mock_external_service_dependencies[ + "feature_service" + ].get_system_features.return_value.license.workspaces.is_available.return_value = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account + mock_account = TestAccountAssociatedDataFactory.create_account_mock() + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.return_value = mock_account + + # Execute test and verify exception + from services.errors.workspace import WorkSpaceNotAllowedCreateError + + with patch("services.account_service.TenantService.create_tenant") as mock_create_tenant: + mock_create_tenant.side_effect = WorkSpaceNotAllowedCreateError() + + self._assert_exception_raised( + AccountRegisterError, + RegisterService.register, + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify rollback was called + mock_db_dependencies["db"].session.rollback.assert_called() + + def test_register_general_exception(self, mock_db_dependencies, mock_external_service_dependencies): + """Test registration with general exception handling.""" + # Setup mocks + mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True + mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False + + # Mock AccountService.create_account to raise exception + with patch("services.account_service.AccountService.create_account") as mock_create_account: + mock_create_account.side_effect = Exception("Unexpected error") + + # Execute test and verify exception + self._assert_exception_raised( + AccountRegisterError, + RegisterService.register, + email="test@example.com", + name="Test User", + password="password123", + language="en-US", + ) + + # Verify rollback was called + mock_db_dependencies["db"].session.rollback.assert_called() + + # ==================== Member Invitation Tests ==================== + + def test_invite_new_member_new_account(self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies): + """Test inviting a new member who doesn't have an account.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + + # Mock database queries - need to mock the Session query + mock_session = MagicMock() + mock_session.query.return_value.filter_by.return_value.first.return_value = None # No existing account + + with patch("services.account_service.Session") as mock_session_class: + mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.__exit__.return_value = None + + # Mock RegisterService.register + mock_new_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="new-user-456", email="newuser@example.com", name="newuser", status="pending" + ) + with patch("services.account_service.RegisterService.register") as mock_register: + mock_register.return_value = mock_new_account + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.TenantService.switch_tenant") as mock_switch_tenant, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + # Execute test + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="newuser@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + # Verify results + assert result == "invite-token-123" + mock_register.assert_called_once_with( + email="newuser@example.com", + name="newuser", + language="en-US", + status="pending", + is_setup=True, + ) + mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal") + mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id) + mock_generate_token.assert_called_once_with(mock_tenant, mock_new_account) + mock_task_dependencies.delay.assert_called_once() + + def test_invite_new_member_existing_account( + self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies + ): + """Test inviting a new member who already has an account.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="pending" + ) + + # Mock database queries - need to mock the Session query + mock_session = MagicMock() + mock_session.query.return_value.filter_by.return_value.first.return_value = mock_existing_account + + with patch("services.account_service.Session") as mock_session_class: + mock_session_class.return_value.__enter__.return_value = mock_session + mock_session_class.return_value.__exit__.return_value = None + + # Mock the db.session.query for TenantAccountJoin + mock_db_query = MagicMock() + mock_db_query.filter_by.return_value.first.return_value = None # No existing member + mock_db_dependencies["db"].session.query.return_value = mock_db_query + + # Mock TenantService methods + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + # Execute test + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + # Verify results + assert result == "invite-token-123" + mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal") + mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account) + mock_task_dependencies.delay.assert_called_once() + + def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies): + """Test inviting a member who is already in the tenant.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="active" + ) + + # Mock database queries + query_results = { + ("Account", "email", "existing@example.com"): mock_existing_account, + ( + "TenantAccountJoin", + "tenant_id", + "tenant-456", + ): TestAccountAssociatedDataFactory.create_tenant_join_mock(), + } + ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results) + + # Mock TenantService methods + with patch("services.account_service.TenantService.check_member_permission") as mock_check_permission: + # Execute test and verify exception + self._assert_exception_raised( + AccountAlreadyInTenantError, + RegisterService.invite_new_member, + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="normal", + inviter=mock_inviter, + ) + + def test_invite_new_member_no_inviter(self): + """Test inviting a member without providing an inviter.""" + # Setup test data + mock_tenant = MagicMock() + + # Execute test and verify exception + self._assert_exception_raised( + ValueError, + RegisterService.invite_new_member, + tenant=mock_tenant, + email="test@example.com", + language="en-US", + role="normal", + inviter=None, + ) + + # ==================== Token Management Tests ==================== + + def test_generate_invite_token_success(self, mock_redis_dependencies): + """Test successful invite token generation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="user-123", email="test@example.com" + ) + + # Mock uuid generation + with patch("services.account_service.uuid.uuid4") as mock_uuid: + mock_uuid.return_value = "test-uuid-123" + + # Execute test + result = RegisterService.generate_invite_token(mock_tenant, mock_account) + + # Verify results + assert result == "test-uuid-123" + mock_redis_dependencies.setex.assert_called_once() + + # Verify the stored data + call_args = mock_redis_dependencies.setex.call_args + assert call_args[0][0] == "member_invite:token:test-uuid-123" + stored_data = json.loads(call_args[0][2]) + assert stored_data["account_id"] == "user-123" + assert stored_data["email"] == "test@example.com" + assert stored_data["workspace_id"] == "tenant-456" + + def test_is_valid_invite_token_valid(self, mock_redis_dependencies): + """Test checking valid invite token.""" + # Setup mock + mock_redis_dependencies.get.return_value = b'{"test": "data"}' + + # Execute test + result = RegisterService.is_valid_invite_token("valid-token") + + # Verify results + assert result is True + mock_redis_dependencies.get.assert_called_once_with("member_invite:token:valid-token") + + def test_is_valid_invite_token_invalid(self, mock_redis_dependencies): + """Test checking invalid invite token.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService.is_valid_invite_token("invalid-token") + + # Verify results + assert result is False + mock_redis_dependencies.get.assert_called_once_with("member_invite:token:invalid-token") + + def test_revoke_token_with_workspace_and_email(self, mock_redis_dependencies): + """Test revoking token with workspace ID and email.""" + # Execute test + RegisterService.revoke_token("workspace-123", "test@example.com", "token-123") + + # Verify results + mock_redis_dependencies.delete.assert_called_once() + call_args = mock_redis_dependencies.delete.call_args + assert "workspace-123" in call_args[0][0] + # The email is hashed, so we check for the hash pattern instead + assert "member_invite_token:" in call_args[0][0] + + def test_revoke_token_without_workspace_and_email(self, mock_redis_dependencies): + """Test revoking token without workspace ID and email.""" + # Execute test + RegisterService.revoke_token("", "", "token-123") + + # Verify results + mock_redis_dependencies.delete.assert_called_once_with("member_invite:token:token-123") + + # ==================== Invitation Validation Tests ==================== + + def test_get_invitation_if_token_valid_success(self, mock_db_dependencies, mock_redis_dependencies): + """Test successful invitation validation.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="user-123", email="test@example.com" + ) + + with patch("services.account_service.RegisterService._get_invitation_by_token") as mock_get_invitation_by_token: + # Mock the invitation data returned by _get_invitation_by_token + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_get_invitation_by_token.return_value = invitation_data + + # Mock database queries - complex query mocking + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = (mock_account, "normal") + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is not None + assert result["account"] == mock_account + assert result["tenant"] == mock_tenant + assert result["data"] == invitation_data + + def test_get_invitation_if_token_valid_no_token_data(self, mock_redis_dependencies): + """Test invitation validation with no token data.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_tenant_not_found(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when tenant is not found.""" + # Setup mock Redis data + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries - no tenant found + mock_query = MagicMock() + mock_query.filter.return_value.first.return_value = None + mock_db_dependencies["db"].session.query.return_value = mock_query + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_account_not_found(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when account is not found.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + + # Mock Redis data + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = None # No account found + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + def test_get_invitation_if_token_valid_account_id_mismatch(self, mock_db_dependencies, mock_redis_dependencies): + """Test invitation validation when account ID doesn't match.""" + # Setup test data + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.status = "normal" + mock_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="different-user-456", email="test@example.com" + ) + + # Mock Redis data with different account ID + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Mock database queries + mock_query1 = MagicMock() + mock_query1.filter.return_value.first.return_value = mock_tenant + + mock_query2 = MagicMock() + mock_query2.join.return_value.filter.return_value.first.return_value = (mock_account, "normal") + + mock_db_dependencies["db"].session.query.side_effect = [mock_query1, mock_query2] + + # Execute test + result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") + + # Verify results + assert result is None + + # ==================== Helper Method Tests ==================== + + def test_get_invitation_token_key(self): + """Test the _get_invitation_token_key helper method.""" + # Execute test + result = RegisterService._get_invitation_token_key("test-token") + + # Verify results + assert result == "member_invite:token:test-token" + + def test_get_invitation_by_token_with_workspace_and_email(self, mock_redis_dependencies): + """Test _get_invitation_by_token with workspace ID and email.""" + # Setup mock + mock_redis_dependencies.get.return_value = b"user-123" + + # Execute test + result = RegisterService._get_invitation_by_token("token-123", "workspace-456", "test@example.com") + + # Verify results + assert result is not None + assert result["account_id"] == "user-123" + assert result["email"] == "test@example.com" + assert result["workspace_id"] == "workspace-456" + + def test_get_invitation_by_token_without_workspace_and_email(self, mock_redis_dependencies): + """Test _get_invitation_by_token without workspace ID and email.""" + # Setup mock + invitation_data = { + "account_id": "user-123", + "email": "test@example.com", + "workspace_id": "tenant-456", + } + mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() + + # Execute test + result = RegisterService._get_invitation_by_token("token-123") + + # Verify results + assert result is not None + assert result == invitation_data + + def test_get_invitation_by_token_no_data(self, mock_redis_dependencies): + """Test _get_invitation_by_token with no data.""" + # Setup mock + mock_redis_dependencies.get.return_value = None + + # Execute test + result = RegisterService._get_invitation_by_token("token-123") + + # Verify results + assert result is None diff --git a/api/tests/unit_tests/services/test_dataset_permission.py b/api/tests/unit_tests/services/test_dataset_permission.py index 066f541c1b..a67252e856 100644 --- a/api/tests/unit_tests/services/test_dataset_permission.py +++ b/api/tests/unit_tests/services/test_dataset_permission.py @@ -8,151 +8,298 @@ from services.dataset_service import DatasetService from services.errors.account import NoPermissionError +class DatasetPermissionTestDataFactory: + """Factory class for creating test data and mock objects for dataset permission tests.""" + + @staticmethod + def create_dataset_mock( + dataset_id: str = "dataset-123", + tenant_id: str = "test-tenant-123", + created_by: str = "creator-456", + permission: DatasetPermissionEnum = DatasetPermissionEnum.ONLY_ME, + **kwargs, + ) -> Mock: + """Create a mock dataset with specified attributes.""" + dataset = Mock(spec=Dataset) + dataset.id = dataset_id + dataset.tenant_id = tenant_id + dataset.created_by = created_by + dataset.permission = permission + for key, value in kwargs.items(): + setattr(dataset, key, value) + return dataset + + @staticmethod + def create_user_mock( + user_id: str = "user-789", + tenant_id: str = "test-tenant-123", + role: TenantAccountRole = TenantAccountRole.NORMAL, + **kwargs, + ) -> Mock: + """Create a mock user with specified attributes.""" + user = Mock(spec=Account) + user.id = user_id + user.current_tenant_id = tenant_id + user.current_role = role + for key, value in kwargs.items(): + setattr(user, key, value) + return user + + @staticmethod + def create_dataset_permission_mock( + dataset_id: str = "dataset-123", + account_id: str = "user-789", + **kwargs, + ) -> Mock: + """Create a mock dataset permission record.""" + permission = Mock(spec=DatasetPermission) + permission.dataset_id = dataset_id + permission.account_id = account_id + for key, value in kwargs.items(): + setattr(permission, key, value) + return permission + + class TestDatasetPermissionService: - """Test cases for dataset permission checking functionality""" - - def setup_method(self): - """Set up test fixtures""" - # Mock tenant and user - self.tenant_id = "test-tenant-123" - self.creator_id = "creator-456" - self.normal_user_id = "normal-789" - self.owner_user_id = "owner-999" - - # Mock dataset - self.dataset = Mock(spec=Dataset) - self.dataset.id = "dataset-123" - self.dataset.tenant_id = self.tenant_id - self.dataset.created_by = self.creator_id - - # Mock users - self.creator_user = Mock(spec=Account) - self.creator_user.id = self.creator_id - self.creator_user.current_tenant_id = self.tenant_id - self.creator_user.current_role = TenantAccountRole.EDITOR - - self.normal_user = Mock(spec=Account) - self.normal_user.id = self.normal_user_id - self.normal_user.current_tenant_id = self.tenant_id - self.normal_user.current_role = TenantAccountRole.NORMAL - - self.owner_user = Mock(spec=Account) - self.owner_user.id = self.owner_user_id - self.owner_user.current_tenant_id = self.tenant_id - self.owner_user.current_role = TenantAccountRole.OWNER + """ + Comprehensive unit tests for DatasetService.check_dataset_permission method. + + This test suite covers all permission scenarios including: + - Cross-tenant access restrictions + - Owner privilege checks + - Different permission levels (ONLY_ME, ALL_TEAM, PARTIAL_TEAM) + - Explicit permission checks for PARTIAL_TEAM + - Error conditions and logging + """ + + @pytest.fixture + def mock_dataset_service_dependencies(self): + """Common mock setup for dataset service dependencies.""" + with patch("services.dataset_service.db.session") as mock_session: + yield { + "db_session": mock_session, + } + + @pytest.fixture + def mock_logging_dependencies(self): + """Mock setup for logging tests.""" + with patch("services.dataset_service.logging") as mock_logging: + yield { + "logging": mock_logging, + } + + def _assert_permission_check_passes(self, dataset: Mock, user: Mock): + """Helper method to verify that permission check passes without raising exceptions.""" + # Should not raise any exception + DatasetService.check_dataset_permission(dataset, user) + + def _assert_permission_check_fails( + self, dataset: Mock, user: Mock, expected_message: str = "You do not have permission to access this dataset." + ): + """Helper method to verify that permission check fails with expected error.""" + with pytest.raises(NoPermissionError, match=expected_message): + DatasetService.check_dataset_permission(dataset, user) + + def _assert_database_query_called(self, mock_session: Mock, dataset_id: str, account_id: str): + """Helper method to verify database query calls for permission checks.""" + mock_session.query().filter_by.assert_called_with(dataset_id=dataset_id, account_id=account_id) + + def _assert_database_query_not_called(self, mock_session: Mock): + """Helper method to verify that database query was not called.""" + mock_session.query.assert_not_called() + + # ==================== Cross-Tenant Access Tests ==================== def test_permission_check_different_tenant_should_fail(self): - """Test that users from different tenants cannot access dataset""" - self.normal_user.current_tenant_id = "different-tenant" + """Test that users from different tenants cannot access dataset regardless of other permissions.""" + # Create dataset and user from different tenants + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + tenant_id="tenant-123", permission=DatasetPermissionEnum.ALL_TEAM + ) + user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="user-789", tenant_id="different-tenant-456", role=TenantAccountRole.EDITOR + ) + + # Should fail due to different tenant + self._assert_permission_check_fails(dataset, user) - with pytest.raises(NoPermissionError, match="You do not have permission to access this dataset."): - DatasetService.check_dataset_permission(self.dataset, self.normal_user) + # ==================== Owner Privilege Tests ==================== def test_owner_can_access_any_dataset(self): - """Test that tenant owners can access any dataset regardless of permission""" - self.dataset.permission = DatasetPermissionEnum.ONLY_ME + """Test that tenant owners can access any dataset regardless of permission level.""" + # Create dataset with restrictive permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock(permission=DatasetPermissionEnum.ONLY_ME) - # Should not raise any exception - DatasetService.check_dataset_permission(self.dataset, self.owner_user) + # Create owner user + owner_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="owner-999", role=TenantAccountRole.OWNER + ) + + # Owner should have access regardless of dataset permission + self._assert_permission_check_passes(dataset, owner_user) + + # ==================== ONLY_ME Permission Tests ==================== def test_only_me_permission_creator_can_access(self): - """Test ONLY_ME permission allows only creator to access""" - self.dataset.permission = DatasetPermissionEnum.ONLY_ME + """Test ONLY_ME permission allows only the dataset creator to access.""" + # Create dataset with ONLY_ME permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + created_by="creator-456", permission=DatasetPermissionEnum.ONLY_ME + ) + + # Create creator user + creator_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="creator-456", role=TenantAccountRole.EDITOR + ) # Creator should be able to access - DatasetService.check_dataset_permission(self.dataset, self.creator_user) + self._assert_permission_check_passes(dataset, creator_user) def test_only_me_permission_others_cannot_access(self): - """Test ONLY_ME permission denies access to non-creators""" - self.dataset.permission = DatasetPermissionEnum.ONLY_ME + """Test ONLY_ME permission denies access to non-creators.""" + # Create dataset with ONLY_ME permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + created_by="creator-456", permission=DatasetPermissionEnum.ONLY_ME + ) + + # Create normal user (not the creator) + normal_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="normal-789", role=TenantAccountRole.NORMAL + ) - with pytest.raises(NoPermissionError, match="You do not have permission to access this dataset."): - DatasetService.check_dataset_permission(self.dataset, self.normal_user) + # Non-creator should be denied access + self._assert_permission_check_fails(dataset, normal_user) + + # ==================== ALL_TEAM Permission Tests ==================== def test_all_team_permission_allows_access(self): - """Test ALL_TEAM permission allows any team member to access""" - self.dataset.permission = DatasetPermissionEnum.ALL_TEAM + """Test ALL_TEAM permission allows any team member to access the dataset.""" + # Create dataset with ALL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock(permission=DatasetPermissionEnum.ALL_TEAM) + + # Create different types of team members + normal_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="normal-789", role=TenantAccountRole.NORMAL + ) + editor_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="editor-456", role=TenantAccountRole.EDITOR + ) + + # All team members should have access + self._assert_permission_check_passes(dataset, normal_user) + self._assert_permission_check_passes(dataset, editor_user) - # Should not raise any exception for team members - DatasetService.check_dataset_permission(self.dataset, self.normal_user) - DatasetService.check_dataset_permission(self.dataset, self.creator_user) + # ==================== PARTIAL_TEAM Permission Tests ==================== - @patch("services.dataset_service.db.session") - def test_partial_team_permission_creator_can_access(self, mock_session): - """Test PARTIAL_TEAM permission allows creator to access""" - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM + def test_partial_team_permission_creator_can_access(self, mock_dataset_service_dependencies): + """Test PARTIAL_TEAM permission allows creator to access without database query.""" + # Create dataset with PARTIAL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + created_by="creator-456", permission=DatasetPermissionEnum.PARTIAL_TEAM + ) - # Should not raise any exception for creator - DatasetService.check_dataset_permission(self.dataset, self.creator_user) + # Create creator user + creator_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="creator-456", role=TenantAccountRole.EDITOR + ) - # Should not query database for creator - mock_session.query.assert_not_called() + # Creator should have access without database query + self._assert_permission_check_passes(dataset, creator_user) + self._assert_database_query_not_called(mock_dataset_service_dependencies["db_session"]) - @patch("services.dataset_service.db.session") - def test_partial_team_permission_with_explicit_permission(self, mock_session): - """Test PARTIAL_TEAM permission allows users with explicit permission""" - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM + def test_partial_team_permission_with_explicit_permission(self, mock_dataset_service_dependencies): + """Test PARTIAL_TEAM permission allows users with explicit permission records.""" + # Create dataset with PARTIAL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock(permission=DatasetPermissionEnum.PARTIAL_TEAM) + + # Create normal user (not the creator) + normal_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="normal-789", role=TenantAccountRole.NORMAL + ) # Mock database query to return a permission record - mock_permission = Mock(spec=DatasetPermission) - mock_session.query().filter_by().first.return_value = mock_permission + mock_permission = DatasetPermissionTestDataFactory.create_dataset_permission_mock( + dataset_id=dataset.id, account_id=normal_user.id + ) + mock_dataset_service_dependencies["db_session"].query().filter_by().first.return_value = mock_permission - # Should not raise any exception - DatasetService.check_dataset_permission(self.dataset, self.normal_user) + # User with explicit permission should have access + self._assert_permission_check_passes(dataset, normal_user) + self._assert_database_query_called(mock_dataset_service_dependencies["db_session"], dataset.id, normal_user.id) - # Verify database was queried correctly - mock_session.query().filter_by.assert_called_with(dataset_id=self.dataset.id, account_id=self.normal_user.id) + def test_partial_team_permission_without_explicit_permission(self, mock_dataset_service_dependencies): + """Test PARTIAL_TEAM permission denies users without explicit permission records.""" + # Create dataset with PARTIAL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock(permission=DatasetPermissionEnum.PARTIAL_TEAM) - @patch("services.dataset_service.db.session") - def test_partial_team_permission_without_explicit_permission(self, mock_session): - """Test PARTIAL_TEAM permission denies users without explicit permission""" - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM + # Create normal user (not the creator) + normal_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="normal-789", role=TenantAccountRole.NORMAL + ) # Mock database query to return None (no permission record) - mock_session.query().filter_by().first.return_value = None - - with pytest.raises(NoPermissionError, match="You do not have permission to access this dataset."): - DatasetService.check_dataset_permission(self.dataset, self.normal_user) + mock_dataset_service_dependencies["db_session"].query().filter_by().first.return_value = None - # Verify database was queried correctly - mock_session.query().filter_by.assert_called_with(dataset_id=self.dataset.id, account_id=self.normal_user.id) + # User without explicit permission should be denied access + self._assert_permission_check_fails(dataset, normal_user) + self._assert_database_query_called(mock_dataset_service_dependencies["db_session"], dataset.id, normal_user.id) - @patch("services.dataset_service.db.session") - def test_partial_team_permission_non_creator_without_permission_fails(self, mock_session): - """Test that non-creators without explicit permission are denied access""" - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM + def test_partial_team_permission_non_creator_without_permission_fails(self, mock_dataset_service_dependencies): + """Test that non-creators without explicit permission are denied access to PARTIAL_TEAM datasets.""" + # Create dataset with PARTIAL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + created_by="creator-456", permission=DatasetPermissionEnum.PARTIAL_TEAM + ) # Create a different user (not the creator) - other_user = Mock(spec=Account) - other_user.id = "other-user-123" - other_user.current_tenant_id = self.tenant_id - other_user.current_role = TenantAccountRole.NORMAL + other_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="other-user-123", role=TenantAccountRole.NORMAL + ) # Mock database query to return None (no permission record) - mock_session.query().filter_by().first.return_value = None + mock_dataset_service_dependencies["db_session"].query().filter_by().first.return_value = None + + # Non-creator without explicit permission should be denied access + self._assert_permission_check_fails(dataset, other_user) + self._assert_database_query_called(mock_dataset_service_dependencies["db_session"], dataset.id, other_user.id) - with pytest.raises(NoPermissionError, match="You do not have permission to access this dataset."): - DatasetService.check_dataset_permission(self.dataset, other_user) + # ==================== Enum Usage Tests ==================== def test_partial_team_permission_uses_correct_enum(self): - """Test that the method correctly uses DatasetPermissionEnum.PARTIAL_TEAM""" - # This test ensures we're using the enum instead of string literals - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM + """Test that the method correctly uses DatasetPermissionEnum.PARTIAL_TEAM instead of string literals.""" + # Create dataset with PARTIAL_TEAM permission using enum + dataset = DatasetPermissionTestDataFactory.create_dataset_mock( + created_by="creator-456", permission=DatasetPermissionEnum.PARTIAL_TEAM + ) + + # Create creator user + creator_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="creator-456", role=TenantAccountRole.EDITOR + ) - # Creator should always have access - DatasetService.check_dataset_permission(self.dataset, self.creator_user) + # Creator should always have access regardless of permission level + self._assert_permission_check_passes(dataset, creator_user) - @patch("services.dataset_service.logging") - @patch("services.dataset_service.db.session") - def test_permission_denied_logs_debug_message(self, mock_session, mock_logging): - """Test that permission denied events are logged""" - self.dataset.permission = DatasetPermissionEnum.PARTIAL_TEAM - mock_session.query().filter_by().first.return_value = None + # ==================== Logging Tests ==================== + + def test_permission_denied_logs_debug_message(self, mock_dataset_service_dependencies, mock_logging_dependencies): + """Test that permission denied events are properly logged for debugging purposes.""" + # Create dataset with PARTIAL_TEAM permission + dataset = DatasetPermissionTestDataFactory.create_dataset_mock(permission=DatasetPermissionEnum.PARTIAL_TEAM) + + # Create normal user (not the creator) + normal_user = DatasetPermissionTestDataFactory.create_user_mock( + user_id="normal-789", role=TenantAccountRole.NORMAL + ) + + # Mock database query to return None (no permission record) + mock_dataset_service_dependencies["db_session"].query().filter_by().first.return_value = None + # Attempt permission check (should fail) with pytest.raises(NoPermissionError): - DatasetService.check_dataset_permission(self.dataset, self.normal_user) + DatasetService.check_dataset_permission(dataset, normal_user) - # Verify debug message was logged - mock_logging.debug.assert_called_with( - f"User {self.normal_user.id} does not have permission to access dataset {self.dataset.id}" + # Verify debug message was logged with correct user and dataset information + mock_logging_dependencies["logging"].debug.assert_called_with( + f"User {normal_user.id} does not have permission to access dataset {dataset.id}" ) diff --git a/api/tests/unit_tests/services/test_dataset_service_batch_update_document_status.py b/api/tests/unit_tests/services/test_dataset_service_batch_update_document_status.py new file mode 100644 index 0000000000..dc09aca5b2 --- /dev/null +++ b/api/tests/unit_tests/services/test_dataset_service_batch_update_document_status.py @@ -0,0 +1,804 @@ +import datetime +from typing import Optional + +# Mock redis_client before importing dataset_service +from unittest.mock import Mock, call, patch + +import pytest + +from models.dataset import Dataset, Document +from services.dataset_service import DocumentService +from services.errors.document import DocumentIndexingError +from tests.unit_tests.conftest import redis_mock + + +class DocumentBatchUpdateTestDataFactory: + """Factory class for creating test data and mock objects for document batch update tests.""" + + @staticmethod + def create_dataset_mock(dataset_id: str = "dataset-123", tenant_id: str = "tenant-456") -> Mock: + """Create a mock dataset with specified attributes.""" + dataset = Mock(spec=Dataset) + dataset.id = dataset_id + dataset.tenant_id = tenant_id + return dataset + + @staticmethod + def create_user_mock(user_id: str = "user-789") -> Mock: + """Create a mock user.""" + user = Mock() + user.id = user_id + return user + + @staticmethod + def create_document_mock( + document_id: str = "doc-1", + name: str = "test_document.pdf", + enabled: bool = True, + archived: bool = False, + indexing_status: str = "completed", + completed_at: Optional[datetime.datetime] = None, + **kwargs, + ) -> Mock: + """Create a mock document with specified attributes.""" + document = Mock(spec=Document) + document.id = document_id + document.name = name + document.enabled = enabled + document.archived = archived + document.indexing_status = indexing_status + document.completed_at = completed_at or datetime.datetime.now() + + # Set default values for optional fields + document.disabled_at = None + document.disabled_by = None + document.archived_at = None + document.archived_by = None + document.updated_at = None + + for key, value in kwargs.items(): + setattr(document, key, value) + return document + + @staticmethod + def create_multiple_documents( + document_ids: list[str], enabled: bool = True, archived: bool = False, indexing_status: str = "completed" + ) -> list[Mock]: + """Create multiple mock documents with specified attributes.""" + documents = [] + for doc_id in document_ids: + doc = DocumentBatchUpdateTestDataFactory.create_document_mock( + document_id=doc_id, + name=f"document_{doc_id}.pdf", + enabled=enabled, + archived=archived, + indexing_status=indexing_status, + ) + documents.append(doc) + return documents + + +class TestDatasetServiceBatchUpdateDocumentStatus: + """ + Comprehensive unit tests for DocumentService.batch_update_document_status method. + + This test suite covers all supported actions (enable, disable, archive, un_archive), + error conditions, edge cases, and validates proper interaction with Redis cache, + database operations, and async task triggers. + """ + + @pytest.fixture + def mock_document_service_dependencies(self): + """Common mock setup for document service dependencies.""" + with ( + patch("services.dataset_service.DocumentService.get_document") as mock_get_doc, + patch("extensions.ext_database.db.session") as mock_db, + patch("services.dataset_service.datetime") as mock_datetime, + ): + current_time = datetime.datetime(2023, 1, 1, 12, 0, 0) + mock_datetime.datetime.now.return_value = current_time + mock_datetime.UTC = datetime.UTC + + yield { + "get_document": mock_get_doc, + "db_session": mock_db, + "datetime": mock_datetime, + "current_time": current_time, + } + + @pytest.fixture + def mock_async_task_dependencies(self): + """Mock setup for async task dependencies.""" + with ( + patch("services.dataset_service.add_document_to_index_task") as mock_add_task, + patch("services.dataset_service.remove_document_from_index_task") as mock_remove_task, + ): + yield {"add_task": mock_add_task, "remove_task": mock_remove_task} + + def _assert_document_enabled(self, document: Mock, user_id: str, current_time: datetime.datetime): + """Helper method to verify document was enabled correctly.""" + assert document.enabled == True + assert document.disabled_at is None + assert document.disabled_by is None + assert document.updated_at == current_time.replace(tzinfo=None) + + def _assert_document_disabled(self, document: Mock, user_id: str, current_time: datetime.datetime): + """Helper method to verify document was disabled correctly.""" + assert document.enabled == False + assert document.disabled_at == current_time.replace(tzinfo=None) + assert document.disabled_by == user_id + assert document.updated_at == current_time.replace(tzinfo=None) + + def _assert_document_archived(self, document: Mock, user_id: str, current_time: datetime.datetime): + """Helper method to verify document was archived correctly.""" + assert document.archived == True + assert document.archived_at == current_time.replace(tzinfo=None) + assert document.archived_by == user_id + assert document.updated_at == current_time.replace(tzinfo=None) + + def _assert_document_unarchived(self, document: Mock): + """Helper method to verify document was unarchived correctly.""" + assert document.archived == False + assert document.archived_at is None + assert document.archived_by is None + + def _assert_redis_cache_operations(self, document_ids: list[str], action: str = "setex"): + """Helper method to verify Redis cache operations.""" + if action == "setex": + expected_calls = [call(f"document_{doc_id}_indexing", 600, 1) for doc_id in document_ids] + redis_mock.setex.assert_has_calls(expected_calls) + elif action == "get": + expected_calls = [call(f"document_{doc_id}_indexing") for doc_id in document_ids] + redis_mock.get.assert_has_calls(expected_calls) + + def _assert_async_task_calls(self, mock_task, document_ids: list[str], task_type: str): + """Helper method to verify async task calls.""" + expected_calls = [call(doc_id) for doc_id in document_ids] + if task_type in {"add", "remove"}: + mock_task.delay.assert_has_calls(expected_calls) + + # ==================== Enable Document Tests ==================== + + def test_batch_update_enable_documents_success( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test successful enabling of disabled documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create disabled documents + disabled_docs = DocumentBatchUpdateTestDataFactory.create_multiple_documents(["doc-1", "doc-2"], enabled=False) + mock_document_service_dependencies["get_document"].side_effect = disabled_docs + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Call the method to enable documents + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1", "doc-2"], action="enable", user=user + ) + + # Verify document attributes were updated correctly + for doc in disabled_docs: + self._assert_document_enabled(doc, user.id, mock_document_service_dependencies["current_time"]) + + # Verify Redis cache operations + self._assert_redis_cache_operations(["doc-1", "doc-2"], "get") + self._assert_redis_cache_operations(["doc-1", "doc-2"], "setex") + + # Verify async tasks were triggered for indexing + self._assert_async_task_calls(mock_async_task_dependencies["add_task"], ["doc-1", "doc-2"], "add") + + # Verify database operations + mock_db = mock_document_service_dependencies["db_session"] + assert mock_db.add.call_count == 2 + assert mock_db.commit.call_count == 1 + + def test_batch_update_enable_already_enabled_document_skipped(self, mock_document_service_dependencies): + """Test enabling documents that are already enabled.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create already enabled document + enabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True) + mock_document_service_dependencies["get_document"].return_value = enabled_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Attempt to enable already enabled document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="enable", user=user + ) + + # Verify no database operations occurred (document was skipped) + mock_db = mock_document_service_dependencies["db_session"] + mock_db.commit.assert_not_called() + + # Verify no Redis setex operations occurred (document was skipped) + redis_mock.setex.assert_not_called() + + # ==================== Disable Document Tests ==================== + + def test_batch_update_disable_documents_success( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test successful disabling of enabled and completed documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create enabled documents + enabled_docs = DocumentBatchUpdateTestDataFactory.create_multiple_documents(["doc-1", "doc-2"], enabled=True) + mock_document_service_dependencies["get_document"].side_effect = enabled_docs + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Call the method to disable documents + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1", "doc-2"], action="disable", user=user + ) + + # Verify document attributes were updated correctly + for doc in enabled_docs: + self._assert_document_disabled(doc, user.id, mock_document_service_dependencies["current_time"]) + + # Verify Redis cache operations for indexing prevention + self._assert_redis_cache_operations(["doc-1", "doc-2"], "setex") + + # Verify async tasks were triggered to remove from index + self._assert_async_task_calls(mock_async_task_dependencies["remove_task"], ["doc-1", "doc-2"], "remove") + + # Verify database operations + mock_db = mock_document_service_dependencies["db_session"] + assert mock_db.add.call_count == 2 + assert mock_db.commit.call_count == 1 + + def test_batch_update_disable_already_disabled_document_skipped( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test disabling documents that are already disabled.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create already disabled document + disabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=False) + mock_document_service_dependencies["get_document"].return_value = disabled_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Attempt to disable already disabled document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="disable", user=user + ) + + # Verify no database operations occurred (document was skipped) + mock_db = mock_document_service_dependencies["db_session"] + mock_db.commit.assert_not_called() + + # Verify no Redis setex operations occurred (document was skipped) + redis_mock.setex.assert_not_called() + + # Verify no async tasks were triggered (document was skipped) + mock_async_task_dependencies["add_task"].delay.assert_not_called() + + def test_batch_update_disable_non_completed_document_error(self, mock_document_service_dependencies): + """Test that DocumentIndexingError is raised when trying to disable non-completed documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create a document that's not completed + non_completed_doc = DocumentBatchUpdateTestDataFactory.create_document_mock( + enabled=True, + indexing_status="indexing", # Not completed + completed_at=None, # Not completed + ) + mock_document_service_dependencies["get_document"].return_value = non_completed_doc + + # Verify that DocumentIndexingError is raised + with pytest.raises(DocumentIndexingError) as exc_info: + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="disable", user=user + ) + + # Verify error message indicates document is not completed + assert "is not completed" in str(exc_info.value) + + # ==================== Archive Document Tests ==================== + + def test_batch_update_archive_documents_success( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test successful archiving of unarchived documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create unarchived enabled document + unarchived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True, archived=False) + mock_document_service_dependencies["get_document"].return_value = unarchived_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Call the method to archive documents + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="archive", user=user + ) + + # Verify document attributes were updated correctly + self._assert_document_archived(unarchived_doc, user.id, mock_document_service_dependencies["current_time"]) + + # Verify Redis cache was set (because document was enabled) + redis_mock.setex.assert_called_once_with("document_doc-1_indexing", 600, 1) + + # Verify async task was triggered to remove from index (because enabled) + mock_async_task_dependencies["remove_task"].delay.assert_called_once_with("doc-1") + + # Verify database operations + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + + def test_batch_update_archive_already_archived_document_skipped(self, mock_document_service_dependencies): + """Test archiving documents that are already archived.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create already archived document + archived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True, archived=True) + mock_document_service_dependencies["get_document"].return_value = archived_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Attempt to archive already archived document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-3"], action="archive", user=user + ) + + # Verify no database operations occurred (document was skipped) + mock_db = mock_document_service_dependencies["db_session"] + mock_db.commit.assert_not_called() + + # Verify no Redis setex operations occurred (document was skipped) + redis_mock.setex.assert_not_called() + + def test_batch_update_archive_disabled_document_no_index_removal( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test archiving disabled documents (should not trigger index removal).""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Set up disabled, unarchived document + disabled_unarchived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=False, archived=False) + mock_document_service_dependencies["get_document"].return_value = disabled_unarchived_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Archive the disabled document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="archive", user=user + ) + + # Verify document was archived + self._assert_document_archived( + disabled_unarchived_doc, user.id, mock_document_service_dependencies["current_time"] + ) + + # Verify no Redis cache was set (document is disabled) + redis_mock.setex.assert_not_called() + + # Verify no index removal task was triggered (document is disabled) + mock_async_task_dependencies["remove_task"].delay.assert_not_called() + + # Verify database operations still occurred + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + + # ==================== Unarchive Document Tests ==================== + + def test_batch_update_unarchive_documents_success( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test successful unarchiving of archived documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create mock archived document + archived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True, archived=True) + mock_document_service_dependencies["get_document"].return_value = archived_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Call the method to unarchive documents + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="un_archive", user=user + ) + + # Verify document attributes were updated correctly + self._assert_document_unarchived(archived_doc) + assert archived_doc.updated_at == mock_document_service_dependencies["current_time"].replace(tzinfo=None) + + # Verify Redis cache was set (because document is enabled) + redis_mock.setex.assert_called_once_with("document_doc-1_indexing", 600, 1) + + # Verify async task was triggered to add back to index (because enabled) + mock_async_task_dependencies["add_task"].delay.assert_called_once_with("doc-1") + + # Verify database operations + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + + def test_batch_update_unarchive_already_unarchived_document_skipped( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test unarchiving documents that are already unarchived.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create already unarchived document + unarchived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True, archived=False) + mock_document_service_dependencies["get_document"].return_value = unarchived_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Attempt to unarchive already unarchived document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="un_archive", user=user + ) + + # Verify no database operations occurred (document was skipped) + mock_db = mock_document_service_dependencies["db_session"] + mock_db.commit.assert_not_called() + + # Verify no Redis setex operations occurred (document was skipped) + redis_mock.setex.assert_not_called() + + # Verify no async tasks were triggered (document was skipped) + mock_async_task_dependencies["add_task"].delay.assert_not_called() + + def test_batch_update_unarchive_disabled_document_no_index_addition( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test unarchiving disabled documents (should not trigger index addition).""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create mock archived but disabled document + archived_disabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=False, archived=True) + mock_document_service_dependencies["get_document"].return_value = archived_disabled_doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Unarchive the disabled document + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="un_archive", user=user + ) + + # Verify document was unarchived + self._assert_document_unarchived(archived_disabled_doc) + assert archived_disabled_doc.updated_at == mock_document_service_dependencies["current_time"].replace( + tzinfo=None + ) + + # Verify no Redis cache was set (document is disabled) + redis_mock.setex.assert_not_called() + + # Verify no index addition task was triggered (document is disabled) + mock_async_task_dependencies["add_task"].delay.assert_not_called() + + # Verify database operations still occurred + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + + # ==================== Error Handling Tests ==================== + + def test_batch_update_document_indexing_error_redis_cache_hit(self, mock_document_service_dependencies): + """Test that DocumentIndexingError is raised when documents are currently being indexed.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create mock enabled document + enabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True) + mock_document_service_dependencies["get_document"].return_value = enabled_doc + + # Set up mock to indicate document is being indexed + redis_mock.reset_mock() + redis_mock.get.return_value = "indexing" + + # Verify that DocumentIndexingError is raised + with pytest.raises(DocumentIndexingError) as exc_info: + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="enable", user=user + ) + + # Verify error message contains document name + assert "test_document.pdf" in str(exc_info.value) + assert "is being indexed" in str(exc_info.value) + + # Verify Redis cache was checked + redis_mock.get.assert_called_once_with("document_doc-1_indexing") + + def test_batch_update_invalid_action_error(self, mock_document_service_dependencies): + """Test that ValueError is raised when an invalid action is provided.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create mock document + doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=True) + mock_document_service_dependencies["get_document"].return_value = doc + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Test with invalid action + invalid_action = "invalid_action" + with pytest.raises(ValueError) as exc_info: + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action=invalid_action, user=user + ) + + # Verify error message contains the invalid action + assert invalid_action in str(exc_info.value) + assert "Invalid action" in str(exc_info.value) + + # Verify no Redis operations occurred + redis_mock.setex.assert_not_called() + + def test_batch_update_async_task_error_handling( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test handling of async task errors during batch operations.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create mock disabled document + disabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock(enabled=False) + mock_document_service_dependencies["get_document"].return_value = disabled_doc + + # Mock async task to raise an exception + mock_async_task_dependencies["add_task"].delay.side_effect = Exception("Celery task error") + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Verify that async task error is propagated + with pytest.raises(Exception) as exc_info: + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1"], action="enable", user=user + ) + + # Verify error message + assert "Celery task error" in str(exc_info.value) + + # Verify database operations completed successfully + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + + # Verify Redis cache was set successfully + redis_mock.setex.assert_called_once_with("document_doc-1_indexing", 600, 1) + + # Verify document was updated + self._assert_document_enabled(disabled_doc, user.id, mock_document_service_dependencies["current_time"]) + + # ==================== Edge Case Tests ==================== + + def test_batch_update_empty_document_list(self, mock_document_service_dependencies): + """Test batch operations with an empty document ID list.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Call method with empty document list + result = DocumentService.batch_update_document_status( + dataset=dataset, document_ids=[], action="enable", user=user + ) + + # Verify no document lookups were performed + mock_document_service_dependencies["get_document"].assert_not_called() + + # Verify method returns None (early return) + assert result is None + + def test_batch_update_document_not_found_skipped(self, mock_document_service_dependencies): + """Test behavior when some documents don't exist in the database.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Mock document service to return None (document not found) + mock_document_service_dependencies["get_document"].return_value = None + + # Call method with non-existent document ID + # This should not raise an error, just skip the missing document + try: + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["non-existent-doc"], action="enable", user=user + ) + except Exception as e: + pytest.fail(f"Method should not raise exception for missing documents: {e}") + + # Verify document lookup was attempted + mock_document_service_dependencies["get_document"].assert_called_once_with(dataset.id, "non-existent-doc") + + def test_batch_update_mixed_document_states_and_actions( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test batch operations on documents with mixed states and various scenarios.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create documents in various states + disabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock("doc-1", enabled=False) + enabled_doc = DocumentBatchUpdateTestDataFactory.create_document_mock("doc-2", enabled=True) + archived_doc = DocumentBatchUpdateTestDataFactory.create_document_mock("doc-3", enabled=True, archived=True) + + # Mix of different document states + documents = [disabled_doc, enabled_doc, archived_doc] + mock_document_service_dependencies["get_document"].side_effect = documents + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Perform enable operation on mixed state documents + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=["doc-1", "doc-2", "doc-3"], action="enable", user=user + ) + + # Verify only the disabled document was processed + # (enabled and archived documents should be skipped for enable action) + + # Only one add should occur (for the disabled document that was enabled) + mock_db = mock_document_service_dependencies["db_session"] + mock_db.add.assert_called_once() + # Only one commit should occur + mock_db.commit.assert_called_once() + + # Only one Redis setex should occur (for the document that was enabled) + redis_mock.setex.assert_called_once_with("document_doc-1_indexing", 600, 1) + + # Only one async task should be triggered (for the document that was enabled) + mock_async_task_dependencies["add_task"].delay.assert_called_once_with("doc-1") + + # ==================== Performance Tests ==================== + + def test_batch_update_large_document_list_performance( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test batch operations with a large number of documents.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create large list of document IDs + document_ids = [f"doc-{i}" for i in range(1, 101)] # 100 documents + + # Create mock documents + mock_documents = DocumentBatchUpdateTestDataFactory.create_multiple_documents( + document_ids, + enabled=False, # All disabled, will be enabled + ) + mock_document_service_dependencies["get_document"].side_effect = mock_documents + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Perform batch enable operation + DocumentService.batch_update_document_status( + dataset=dataset, document_ids=document_ids, action="enable", user=user + ) + + # Verify all documents were processed + assert mock_document_service_dependencies["get_document"].call_count == 100 + + # Verify all documents were updated + for mock_doc in mock_documents: + self._assert_document_enabled(mock_doc, user.id, mock_document_service_dependencies["current_time"]) + + # Verify database operations + mock_db = mock_document_service_dependencies["db_session"] + assert mock_db.add.call_count == 100 + assert mock_db.commit.call_count == 1 + + # Verify Redis cache operations occurred for each document + assert redis_mock.setex.call_count == 100 + + # Verify async tasks were triggered for each document + assert mock_async_task_dependencies["add_task"].delay.call_count == 100 + + # Verify correct Redis cache keys were set + expected_redis_calls = [call(f"document_doc-{i}_indexing", 600, 1) for i in range(1, 101)] + redis_mock.setex.assert_has_calls(expected_redis_calls) + + # Verify correct async task calls + expected_task_calls = [call(f"doc-{i}") for i in range(1, 101)] + mock_async_task_dependencies["add_task"].delay.assert_has_calls(expected_task_calls) + + def test_batch_update_mixed_document_states_complex_scenario( + self, mock_document_service_dependencies, mock_async_task_dependencies + ): + """Test complex batch operations with documents in various states.""" + dataset = DocumentBatchUpdateTestDataFactory.create_dataset_mock() + user = DocumentBatchUpdateTestDataFactory.create_user_mock() + + # Create documents in various states + doc1 = DocumentBatchUpdateTestDataFactory.create_document_mock("doc-1", enabled=False) # Will be enabled + doc2 = DocumentBatchUpdateTestDataFactory.create_document_mock( + "doc-2", enabled=True + ) # Already enabled, will be skipped + doc3 = DocumentBatchUpdateTestDataFactory.create_document_mock( + "doc-3", enabled=True + ) # Already enabled, will be skipped + doc4 = DocumentBatchUpdateTestDataFactory.create_document_mock( + "doc-4", enabled=True + ) # Not affected by enable action + doc5 = DocumentBatchUpdateTestDataFactory.create_document_mock( + "doc-5", enabled=True, archived=True + ) # Not affected by enable action + doc6 = None # Non-existent, will be skipped + + mock_document_service_dependencies["get_document"].side_effect = [doc1, doc2, doc3, doc4, doc5, doc6] + + # Reset module-level Redis mock + redis_mock.reset_mock() + redis_mock.get.return_value = None + + # Perform mixed batch operations + DocumentService.batch_update_document_status( + dataset=dataset, + document_ids=["doc-1", "doc-2", "doc-3", "doc-4", "doc-5", "doc-6"], + action="enable", # This will only affect doc1 + user=user, + ) + + # Verify document 1 was enabled + self._assert_document_enabled(doc1, user.id, mock_document_service_dependencies["current_time"]) + + # Verify other documents were skipped appropriately + assert doc2.enabled == True # No change + assert doc3.enabled == True # No change + assert doc4.enabled == True # No change + assert doc5.enabled == True # No change + + # Verify database commits occurred for processed documents + # Only doc1 should be added (others were skipped, doc6 doesn't exist) + mock_db = mock_document_service_dependencies["db_session"] + assert mock_db.add.call_count == 1 + assert mock_db.commit.call_count == 1 + + # Verify Redis cache operations occurred for processed documents + # Only doc1 should have Redis operations + assert redis_mock.setex.call_count == 1 + + # Verify async tasks were triggered for processed documents + # Only doc1 should trigger tasks + assert mock_async_task_dependencies["add_task"].delay.call_count == 1 + + # Verify correct Redis cache keys were set + expected_redis_calls = [call("document_doc-1_indexing", 600, 1)] + redis_mock.setex.assert_has_calls(expected_redis_calls) + + # Verify correct async task calls + expected_task_calls = [call("doc-1")] + mock_async_task_dependencies["add_task"].delay.assert_has_calls(expected_task_calls) diff --git a/api/tests/unit_tests/services/test_dataset_service_update_dataset.py b/api/tests/unit_tests/services/test_dataset_service_update_dataset.py new file mode 100644 index 0000000000..87b46f213b --- /dev/null +++ b/api/tests/unit_tests/services/test_dataset_service_update_dataset.py @@ -0,0 +1,632 @@ +import datetime +from typing import Any, Optional + +# Mock redis_client before importing dataset_service +from unittest.mock import Mock, patch + +import pytest + +from core.model_runtime.entities.model_entities import ModelType +from models.dataset import Dataset, ExternalKnowledgeBindings +from services.dataset_service import DatasetService +from services.errors.account import NoPermissionError + + +class DatasetUpdateTestDataFactory: + """Factory class for creating test data and mock objects for dataset update tests.""" + + @staticmethod + def create_dataset_mock( + dataset_id: str = "dataset-123", + provider: str = "vendor", + name: str = "old_name", + description: str = "old_description", + indexing_technique: str = "high_quality", + retrieval_model: str = "old_model", + embedding_model_provider: Optional[str] = None, + embedding_model: Optional[str] = None, + collection_binding_id: Optional[str] = None, + **kwargs, + ) -> Mock: + """Create a mock dataset with specified attributes.""" + dataset = Mock(spec=Dataset) + dataset.id = dataset_id + dataset.provider = provider + dataset.name = name + dataset.description = description + dataset.indexing_technique = indexing_technique + dataset.retrieval_model = retrieval_model + dataset.embedding_model_provider = embedding_model_provider + dataset.embedding_model = embedding_model + dataset.collection_binding_id = collection_binding_id + for key, value in kwargs.items(): + setattr(dataset, key, value) + return dataset + + @staticmethod + def create_user_mock(user_id: str = "user-789") -> Mock: + """Create a mock user.""" + user = Mock() + user.id = user_id + return user + + @staticmethod + def create_external_binding_mock( + external_knowledge_id: str = "old_knowledge_id", external_knowledge_api_id: str = "old_api_id" + ) -> Mock: + """Create a mock external knowledge binding.""" + binding = Mock(spec=ExternalKnowledgeBindings) + binding.external_knowledge_id = external_knowledge_id + binding.external_knowledge_api_id = external_knowledge_api_id + return binding + + @staticmethod + def create_embedding_model_mock(model: str = "text-embedding-ada-002", provider: str = "openai") -> Mock: + """Create a mock embedding model.""" + embedding_model = Mock() + embedding_model.model = model + embedding_model.provider = provider + return embedding_model + + @staticmethod + def create_collection_binding_mock(binding_id: str = "binding-456") -> Mock: + """Create a mock collection binding.""" + binding = Mock() + binding.id = binding_id + return binding + + @staticmethod + def create_current_user_mock(tenant_id: str = "tenant-123") -> Mock: + """Create a mock current user.""" + current_user = Mock() + current_user.current_tenant_id = tenant_id + return current_user + + +class TestDatasetServiceUpdateDataset: + """ + Comprehensive unit tests for DatasetService.update_dataset method. + + This test suite covers all supported scenarios including: + - External dataset updates + - Internal dataset updates with different indexing techniques + - Embedding model updates + - Permission checks + - Error conditions and edge cases + """ + + @pytest.fixture + def mock_dataset_service_dependencies(self): + """Common mock setup for dataset service dependencies.""" + with ( + patch("services.dataset_service.DatasetService.get_dataset") as mock_get_dataset, + patch("services.dataset_service.DatasetService.check_dataset_permission") as mock_check_perm, + patch("extensions.ext_database.db.session") as mock_db, + patch("services.dataset_service.datetime") as mock_datetime, + ): + current_time = datetime.datetime(2023, 1, 1, 12, 0, 0) + mock_datetime.datetime.now.return_value = current_time + mock_datetime.UTC = datetime.UTC + + yield { + "get_dataset": mock_get_dataset, + "check_permission": mock_check_perm, + "db_session": mock_db, + "datetime": mock_datetime, + "current_time": current_time, + } + + @pytest.fixture + def mock_external_provider_dependencies(self): + """Mock setup for external provider tests.""" + with patch("services.dataset_service.Session") as mock_session: + from extensions.ext_database import db + + with patch.object(db.__class__, "engine", new_callable=Mock): + session_mock = Mock() + mock_session.return_value.__enter__.return_value = session_mock + yield session_mock + + @pytest.fixture + def mock_internal_provider_dependencies(self): + """Mock setup for internal provider tests.""" + with ( + patch("services.dataset_service.ModelManager") as mock_model_manager, + patch( + "services.dataset_service.DatasetCollectionBindingService.get_dataset_collection_binding" + ) as mock_get_binding, + patch("services.dataset_service.deal_dataset_vector_index_task") as mock_task, + patch("services.dataset_service.current_user") as mock_current_user, + ): + mock_current_user.current_tenant_id = "tenant-123" + yield { + "model_manager": mock_model_manager, + "get_binding": mock_get_binding, + "task": mock_task, + "current_user": mock_current_user, + } + + def _assert_database_update_called(self, mock_db, dataset_id: str, expected_updates: dict[str, Any]): + """Helper method to verify database update calls.""" + mock_db.query.return_value.filter_by.return_value.update.assert_called_once_with(expected_updates) + mock_db.commit.assert_called_once() + + def _assert_external_dataset_update(self, mock_dataset, mock_binding, update_data: dict[str, Any]): + """Helper method to verify external dataset updates.""" + assert mock_dataset.name == update_data.get("name", mock_dataset.name) + assert mock_dataset.description == update_data.get("description", mock_dataset.description) + assert mock_dataset.retrieval_model == update_data.get("external_retrieval_model", mock_dataset.retrieval_model) + + if "external_knowledge_id" in update_data: + assert mock_binding.external_knowledge_id == update_data["external_knowledge_id"] + if "external_knowledge_api_id" in update_data: + assert mock_binding.external_knowledge_api_id == update_data["external_knowledge_api_id"] + + # ==================== External Dataset Tests ==================== + + def test_update_external_dataset_success( + self, mock_dataset_service_dependencies, mock_external_provider_dependencies + ): + """Test successful update of external dataset.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock( + provider="external", name="old_name", description="old_description", retrieval_model="old_model" + ) + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + binding = DatasetUpdateTestDataFactory.create_external_binding_mock() + + # Mock external knowledge binding query + mock_external_provider_dependencies.query.return_value.filter_by.return_value.first.return_value = binding + + update_data = { + "name": "new_name", + "description": "new_description", + "external_retrieval_model": "new_model", + "permission": "only_me", + "external_knowledge_id": "new_knowledge_id", + "external_knowledge_api_id": "new_api_id", + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify permission check was called + mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user) + + # Verify dataset and binding updates + self._assert_external_dataset_update(dataset, binding, update_data) + + # Verify database operations + mock_db = mock_dataset_service_dependencies["db_session"] + mock_db.add.assert_any_call(dataset) + mock_db.add.assert_any_call(binding) + mock_db.commit.assert_called_once() + + # Verify return value + assert result == dataset + + def test_update_external_dataset_missing_knowledge_id_error(self, mock_dataset_service_dependencies): + """Test error when external knowledge id is missing.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="external") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + update_data = {"name": "new_name", "external_knowledge_api_id": "api_id"} + + with pytest.raises(ValueError) as context: + DatasetService.update_dataset("dataset-123", update_data, user) + + assert "External knowledge id is required" in str(context.value) + + def test_update_external_dataset_missing_api_id_error(self, mock_dataset_service_dependencies): + """Test error when external knowledge api id is missing.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="external") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + update_data = {"name": "new_name", "external_knowledge_id": "knowledge_id"} + + with pytest.raises(ValueError) as context: + DatasetService.update_dataset("dataset-123", update_data, user) + + assert "External knowledge api id is required" in str(context.value) + + def test_update_external_dataset_binding_not_found_error( + self, mock_dataset_service_dependencies, mock_external_provider_dependencies + ): + """Test error when external knowledge binding is not found.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="external") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + # Mock external knowledge binding query returning None + mock_external_provider_dependencies.query.return_value.filter_by.return_value.first.return_value = None + + update_data = { + "name": "new_name", + "external_knowledge_id": "knowledge_id", + "external_knowledge_api_id": "api_id", + } + + with pytest.raises(ValueError) as context: + DatasetService.update_dataset("dataset-123", update_data, user) + + assert "External knowledge binding not found" in str(context.value) + + # ==================== Internal Dataset Basic Tests ==================== + + def test_update_internal_dataset_basic_success(self, mock_dataset_service_dependencies): + """Test successful update of internal dataset with basic fields.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock( + provider="vendor", + indexing_technique="high_quality", + embedding_model_provider="openai", + embedding_model="text-embedding-ada-002", + collection_binding_id="binding-123", + ) + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + update_data = { + "name": "new_name", + "description": "new_description", + "indexing_technique": "high_quality", + "retrieval_model": "new_model", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-ada-002", + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify permission check was called + mock_dataset_service_dependencies["check_permission"].assert_called_once_with(dataset, user) + + # Verify database update was called with correct filtered data + expected_filtered_data = { + "name": "new_name", + "description": "new_description", + "indexing_technique": "high_quality", + "retrieval_model": "new_model", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-ada-002", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify return value + assert result == dataset + + def test_update_internal_dataset_filter_none_values(self, mock_dataset_service_dependencies): + """Test that None values are filtered out except for description field.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="vendor", indexing_technique="high_quality") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + update_data = { + "name": "new_name", + "description": None, # Should be included + "indexing_technique": "high_quality", + "retrieval_model": "new_model", + "embedding_model_provider": None, # Should be filtered out + "embedding_model": None, # Should be filtered out + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify database update was called with filtered data + expected_filtered_data = { + "name": "new_name", + "description": None, # Description should be included even if None + "indexing_technique": "high_quality", + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + actual_call_args = mock_dataset_service_dependencies[ + "db_session" + ].query.return_value.filter_by.return_value.update.call_args[0][0] + # Remove timestamp for comparison as it's dynamic + del actual_call_args["updated_at"] + del expected_filtered_data["updated_at"] + + assert actual_call_args == expected_filtered_data + + # Verify return value + assert result == dataset + + # ==================== Indexing Technique Switch Tests ==================== + + def test_update_internal_dataset_indexing_technique_to_economy( + self, mock_dataset_service_dependencies, mock_internal_provider_dependencies + ): + """Test updating internal dataset indexing technique to economy.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="vendor", indexing_technique="high_quality") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + update_data = {"indexing_technique": "economy", "retrieval_model": "new_model"} + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify database update was called with embedding model fields cleared + expected_filtered_data = { + "indexing_technique": "economy", + "embedding_model": None, + "embedding_model_provider": None, + "collection_binding_id": None, + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify return value + assert result == dataset + + def test_update_internal_dataset_indexing_technique_to_high_quality( + self, mock_dataset_service_dependencies, mock_internal_provider_dependencies + ): + """Test updating internal dataset indexing technique to high_quality.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="vendor", indexing_technique="economy") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + # Mock embedding model + embedding_model = DatasetUpdateTestDataFactory.create_embedding_model_mock() + mock_internal_provider_dependencies[ + "model_manager" + ].return_value.get_model_instance.return_value = embedding_model + + # Mock collection binding + binding = DatasetUpdateTestDataFactory.create_collection_binding_mock() + mock_internal_provider_dependencies["get_binding"].return_value = binding + + update_data = { + "indexing_technique": "high_quality", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-ada-002", + "retrieval_model": "new_model", + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify embedding model was validated + mock_internal_provider_dependencies["model_manager"].return_value.get_model_instance.assert_called_once_with( + tenant_id=mock_internal_provider_dependencies["current_user"].current_tenant_id, + provider="openai", + model_type=ModelType.TEXT_EMBEDDING, + model="text-embedding-ada-002", + ) + + # Verify collection binding was retrieved + mock_internal_provider_dependencies["get_binding"].assert_called_once_with("openai", "text-embedding-ada-002") + + # Verify database update was called with correct data + expected_filtered_data = { + "indexing_technique": "high_quality", + "embedding_model": "text-embedding-ada-002", + "embedding_model_provider": "openai", + "collection_binding_id": "binding-456", + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify vector index task was triggered + mock_internal_provider_dependencies["task"].delay.assert_called_once_with("dataset-123", "add") + + # Verify return value + assert result == dataset + + # ==================== Embedding Model Update Tests ==================== + + def test_update_internal_dataset_keep_existing_embedding_model(self, mock_dataset_service_dependencies): + """Test updating internal dataset without changing embedding model.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock( + provider="vendor", + indexing_technique="high_quality", + embedding_model_provider="openai", + embedding_model="text-embedding-ada-002", + collection_binding_id="binding-123", + ) + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + update_data = {"name": "new_name", "indexing_technique": "high_quality", "retrieval_model": "new_model"} + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify database update was called with existing embedding model preserved + expected_filtered_data = { + "name": "new_name", + "indexing_technique": "high_quality", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-ada-002", + "collection_binding_id": "binding-123", + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify return value + assert result == dataset + + def test_update_internal_dataset_embedding_model_update( + self, mock_dataset_service_dependencies, mock_internal_provider_dependencies + ): + """Test updating internal dataset with new embedding model.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock( + provider="vendor", + indexing_technique="high_quality", + embedding_model_provider="openai", + embedding_model="text-embedding-ada-002", + ) + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + # Mock embedding model + embedding_model = DatasetUpdateTestDataFactory.create_embedding_model_mock("text-embedding-3-small") + mock_internal_provider_dependencies[ + "model_manager" + ].return_value.get_model_instance.return_value = embedding_model + + # Mock collection binding + binding = DatasetUpdateTestDataFactory.create_collection_binding_mock("binding-789") + mock_internal_provider_dependencies["get_binding"].return_value = binding + + update_data = { + "indexing_technique": "high_quality", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-3-small", + "retrieval_model": "new_model", + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify embedding model was validated + mock_internal_provider_dependencies["model_manager"].return_value.get_model_instance.assert_called_once_with( + tenant_id=mock_internal_provider_dependencies["current_user"].current_tenant_id, + provider="openai", + model_type=ModelType.TEXT_EMBEDDING, + model="text-embedding-3-small", + ) + + # Verify collection binding was retrieved + mock_internal_provider_dependencies["get_binding"].assert_called_once_with("openai", "text-embedding-3-small") + + # Verify database update was called with correct data + expected_filtered_data = { + "indexing_technique": "high_quality", + "embedding_model": "text-embedding-3-small", + "embedding_model_provider": "openai", + "collection_binding_id": "binding-789", + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify vector index task was triggered + mock_internal_provider_dependencies["task"].delay.assert_called_once_with("dataset-123", "update") + + # Verify return value + assert result == dataset + + def test_update_internal_dataset_no_indexing_technique_change(self, mock_dataset_service_dependencies): + """Test updating internal dataset without changing indexing technique.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock( + provider="vendor", + indexing_technique="high_quality", + embedding_model_provider="openai", + embedding_model="text-embedding-ada-002", + collection_binding_id="binding-123", + ) + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + update_data = { + "name": "new_name", + "indexing_technique": "high_quality", # Same as current + "retrieval_model": "new_model", + } + + result = DatasetService.update_dataset("dataset-123", update_data, user) + + # Verify database update was called with correct data + expected_filtered_data = { + "name": "new_name", + "indexing_technique": "high_quality", + "embedding_model_provider": "openai", + "embedding_model": "text-embedding-ada-002", + "collection_binding_id": "binding-123", + "retrieval_model": "new_model", + "updated_by": user.id, + "updated_at": mock_dataset_service_dependencies["current_time"].replace(tzinfo=None), + } + + self._assert_database_update_called( + mock_dataset_service_dependencies["db_session"], "dataset-123", expected_filtered_data + ) + + # Verify return value + assert result == dataset + + # ==================== Error Handling Tests ==================== + + def test_update_dataset_not_found_error(self, mock_dataset_service_dependencies): + """Test error when dataset is not found.""" + mock_dataset_service_dependencies["get_dataset"].return_value = None + + user = DatasetUpdateTestDataFactory.create_user_mock() + update_data = {"name": "new_name"} + + with pytest.raises(ValueError) as context: + DatasetService.update_dataset("dataset-123", update_data, user) + + assert "Dataset not found" in str(context.value) + + def test_update_dataset_permission_error(self, mock_dataset_service_dependencies): + """Test error when user doesn't have permission.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock() + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + mock_dataset_service_dependencies["check_permission"].side_effect = NoPermissionError("No permission") + + update_data = {"name": "new_name"} + + with pytest.raises(NoPermissionError): + DatasetService.update_dataset("dataset-123", update_data, user) + + def test_update_internal_dataset_embedding_model_error( + self, mock_dataset_service_dependencies, mock_internal_provider_dependencies + ): + """Test error when embedding model is not available.""" + dataset = DatasetUpdateTestDataFactory.create_dataset_mock(provider="vendor", indexing_technique="economy") + mock_dataset_service_dependencies["get_dataset"].return_value = dataset + + user = DatasetUpdateTestDataFactory.create_user_mock() + + # Mock model manager to raise error + mock_internal_provider_dependencies["model_manager"].return_value.get_model_instance.side_effect = Exception( + "No Embedding Model available" + ) + + update_data = { + "indexing_technique": "high_quality", + "embedding_model_provider": "invalid_provider", + "embedding_model": "invalid_model", + "retrieval_model": "new_model", + } + + with pytest.raises(Exception) as context: + DatasetService.update_dataset("dataset-123", update_data, user) + + assert "No Embedding Model available".lower() in str(context.value).lower() diff --git a/api/tests/unit_tests/services/workflow/test_workflow_deletion.py b/api/tests/unit_tests/services/workflow/test_workflow_deletion.py index 223020c2c5..2c87eaf805 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_deletion.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_deletion.py @@ -10,7 +10,8 @@ from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseE @pytest.fixture def workflow_setup(): - workflow_service = WorkflowService() + mock_session_maker = MagicMock() + workflow_service = WorkflowService(mock_session_maker) session = MagicMock(spec=Session) tenant_id = "test-tenant-id" workflow_id = "test-workflow-id" diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py new file mode 100644 index 0000000000..8b1348b75b --- /dev/null +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -0,0 +1,391 @@ +import dataclasses +import secrets +from unittest.mock import MagicMock, Mock, patch + +import pytest +from sqlalchemy import Engine +from sqlalchemy.orm import Session + +from core.variables import StringSegment +from core.workflow.constants import SYSTEM_VARIABLE_NODE_ID +from core.workflow.nodes.enums import NodeType +from models.enums import DraftVariableType +from models.workflow import Workflow, WorkflowDraftVariable, WorkflowNodeExecutionModel, is_system_variable_editable +from services.workflow_draft_variable_service import ( + DraftVariableSaver, + VariableResetError, + WorkflowDraftVariableService, +) + + +@pytest.fixture +def mock_engine() -> Engine: + return Mock(spec=Engine) + + +@pytest.fixture +def mock_session(mock_engine) -> Session: + mock_session = Mock(spec=Session) + mock_session.get_bind.return_value = mock_engine + return mock_session + + +class TestDraftVariableSaver: + def _get_test_app_id(self): + suffix = secrets.token_hex(6) + return f"test_app_id_{suffix}" + + def test__should_variable_be_visible(self): + mock_session = MagicMock(spec=Session) + test_app_id = self._get_test_app_id() + saver = DraftVariableSaver( + session=mock_session, + app_id=test_app_id, + node_id="test_node_id", + node_type=NodeType.START, + node_execution_id="test_execution_id", + ) + assert saver._should_variable_be_visible("123_456", NodeType.IF_ELSE, "output") == False + assert saver._should_variable_be_visible("123", NodeType.START, "output") == True + + def test__normalize_variable_for_start_node(self): + @dataclasses.dataclass(frozen=True) + class TestCase: + name: str + input_node_id: str + input_name: str + expected_node_id: str + expected_name: str + + _NODE_ID = "1747228642872" + cases = [ + TestCase( + name="name with `sys.` prefix should return the system node_id", + input_node_id=_NODE_ID, + input_name="sys.workflow_id", + expected_node_id=SYSTEM_VARIABLE_NODE_ID, + expected_name="workflow_id", + ), + TestCase( + name="name without `sys.` prefix should return the original input node_id", + input_node_id=_NODE_ID, + input_name="start_input", + expected_node_id=_NODE_ID, + expected_name="start_input", + ), + TestCase( + name="dummy_variable should return the original input node_id", + input_node_id=_NODE_ID, + input_name="__dummy__", + expected_node_id=_NODE_ID, + expected_name="__dummy__", + ), + ] + + mock_session = MagicMock(spec=Session) + test_app_id = self._get_test_app_id() + saver = DraftVariableSaver( + session=mock_session, + app_id=test_app_id, + node_id=_NODE_ID, + node_type=NodeType.START, + node_execution_id="test_execution_id", + ) + for idx, c in enumerate(cases, 1): + fail_msg = f"Test case {c.name} failed, index={idx}" + node_id, name = saver._normalize_variable_for_start_node(c.input_name) + assert node_id == c.expected_node_id, fail_msg + assert name == c.expected_name, fail_msg + + +class TestWorkflowDraftVariableService: + def _get_test_app_id(self): + suffix = secrets.token_hex(6) + return f"test_app_id_{suffix}" + + def _create_test_workflow(self, app_id: str) -> Workflow: + """Create a real Workflow instance for testing""" + return Workflow.new( + tenant_id="test_tenant_id", + app_id=app_id, + type="workflow", + version="draft", + graph='{"nodes": [], "edges": []}', + features="{}", + created_by="test_user_id", + environment_variables=[], + conversation_variables=[], + ) + + def test_reset_conversation_variable(self, mock_session): + """Test resetting a conversation variable""" + service = WorkflowDraftVariableService(mock_session) + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create real conversation variable + test_value = StringSegment(value="test_value") + variable = WorkflowDraftVariable.new_conversation_variable( + app_id=test_app_id, name="test_var", value=test_value, description="Test conversation variable" + ) + + # Mock the _reset_conv_var method + expected_result = WorkflowDraftVariable.new_conversation_variable( + app_id=test_app_id, + name="test_var", + value=StringSegment(value="reset_value"), + ) + with patch.object(service, "_reset_conv_var", return_value=expected_result) as mock_reset_conv: + result = service.reset_variable(workflow, variable) + + mock_reset_conv.assert_called_once_with(workflow, variable) + assert result == expected_result + + def test_reset_node_variable_with_no_execution_id(self, mock_session): + """Test resetting a node variable with no execution ID - should delete variable""" + service = WorkflowDraftVariableService(mock_session) + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create real node variable with no execution ID + test_value = StringSegment(value="test_value") + variable = WorkflowDraftVariable.new_node_variable( + app_id=test_app_id, + node_id="test_node_id", + name="test_var", + value=test_value, + node_execution_id="exec-id", # Set initially + ) + # Manually set to None to simulate the test condition + variable.node_execution_id = None + + result = service._reset_node_var_or_sys_var(workflow, variable) + + # Should delete the variable and return None + mock_session.delete.assert_called_once_with(instance=variable) + mock_session.flush.assert_called_once() + assert result is None + + def test_reset_node_variable_with_missing_execution_record( + self, + mock_engine, + mock_session, + monkeypatch, + ): + """Test resetting a node variable when execution record doesn't exist""" + mock_repo_session = Mock(spec=Session) + + mock_session_maker = MagicMock() + # Mock the context manager protocol for sessionmaker + mock_session_maker.return_value.__enter__.return_value = mock_repo_session + mock_session_maker.return_value.__exit__.return_value = None + monkeypatch.setattr("services.workflow_draft_variable_service.sessionmaker", mock_session_maker) + service = WorkflowDraftVariableService(mock_session) + + # Mock the repository to return None (no execution record found) + service._api_node_execution_repo = Mock() + service._api_node_execution_repo.get_execution_by_id.return_value = None + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create real node variable with execution ID + test_value = StringSegment(value="test_value") + variable = WorkflowDraftVariable.new_node_variable( + app_id=test_app_id, node_id="test_node_id", name="test_var", value=test_value, node_execution_id="exec-id" + ) + # Variable is editable by default from factory method + + result = service._reset_node_var_or_sys_var(workflow, variable) + + mock_session_maker.assert_called_once_with(bind=mock_engine, expire_on_commit=False) + # Should delete the variable and return None + mock_session.delete.assert_called_once_with(instance=variable) + mock_session.flush.assert_called_once() + assert result is None + + def test_reset_node_variable_with_valid_execution_record( + self, + mock_session, + monkeypatch, + ): + """Test resetting a node variable with valid execution record - should restore from execution""" + mock_repo_session = Mock(spec=Session) + + mock_session_maker = MagicMock() + # Mock the context manager protocol for sessionmaker + mock_session_maker.return_value.__enter__.return_value = mock_repo_session + mock_session_maker.return_value.__exit__.return_value = None + mock_session_maker = monkeypatch.setattr( + "services.workflow_draft_variable_service.sessionmaker", mock_session_maker + ) + service = WorkflowDraftVariableService(mock_session) + + # Create mock execution record + mock_execution = Mock(spec=WorkflowNodeExecutionModel) + mock_execution.outputs_dict = {"test_var": "output_value"} + + # Mock the repository to return the execution record + service._api_node_execution_repo = Mock() + service._api_node_execution_repo.get_execution_by_id.return_value = mock_execution + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create real node variable with execution ID + test_value = StringSegment(value="original_value") + variable = WorkflowDraftVariable.new_node_variable( + app_id=test_app_id, node_id="test_node_id", name="test_var", value=test_value, node_execution_id="exec-id" + ) + # Variable is editable by default from factory method + + # Mock workflow methods + mock_node_config = {"type": "test_node"} + with ( + patch.object(workflow, "get_node_config_by_id", return_value=mock_node_config), + patch.object(workflow, "get_node_type_from_node_config", return_value=NodeType.LLM), + ): + result = service._reset_node_var_or_sys_var(workflow, variable) + + # Verify last_edited_at was reset + assert variable.last_edited_at is None + # Verify session.flush was called + mock_session.flush.assert_called() + + # Should return the updated variable + assert result == variable + + def test_reset_non_editable_system_variable_raises_error(self, mock_session): + """Test that resetting a non-editable system variable raises an error""" + service = WorkflowDraftVariableService(mock_session) + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create a non-editable system variable (workflow_id is not editable) + test_value = StringSegment(value="test_workflow_id") + variable = WorkflowDraftVariable.new_sys_variable( + app_id=test_app_id, + name="workflow_id", # This is not in _EDITABLE_SYSTEM_VARIABLE + value=test_value, + node_execution_id="exec-id", + editable=False, # Non-editable system variable + ) + + with pytest.raises(VariableResetError) as exc_info: + service.reset_variable(workflow, variable) + assert "cannot reset system variable" in str(exc_info.value) + assert f"variable_id={variable.id}" in str(exc_info.value) + + def test_reset_editable_system_variable_succeeds(self, mock_session): + """Test that resetting an editable system variable succeeds""" + service = WorkflowDraftVariableService(mock_session) + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create an editable system variable (files is editable) + test_value = StringSegment(value="[]") + variable = WorkflowDraftVariable.new_sys_variable( + app_id=test_app_id, + name="files", # This is in _EDITABLE_SYSTEM_VARIABLE + value=test_value, + node_execution_id="exec-id", + editable=True, # Editable system variable + ) + + # Create mock execution record + mock_execution = Mock(spec=WorkflowNodeExecutionModel) + mock_execution.outputs_dict = {"sys.files": "[]"} + + # Mock the repository to return the execution record + service._api_node_execution_repo = Mock() + service._api_node_execution_repo.get_execution_by_id.return_value = mock_execution + + result = service._reset_node_var_or_sys_var(workflow, variable) + + # Should succeed and return the variable + assert result == variable + assert variable.last_edited_at is None + mock_session.flush.assert_called() + + def test_reset_query_system_variable_succeeds(self, mock_session): + """Test that resetting query system variable (another editable one) succeeds""" + service = WorkflowDraftVariableService(mock_session) + + test_app_id = self._get_test_app_id() + workflow = self._create_test_workflow(test_app_id) + + # Create an editable system variable (query is editable) + test_value = StringSegment(value="original query") + variable = WorkflowDraftVariable.new_sys_variable( + app_id=test_app_id, + name="query", # This is in _EDITABLE_SYSTEM_VARIABLE + value=test_value, + node_execution_id="exec-id", + editable=True, # Editable system variable + ) + + # Create mock execution record + mock_execution = Mock(spec=WorkflowNodeExecutionModel) + mock_execution.outputs_dict = {"sys.query": "reset query"} + + # Mock the repository to return the execution record + service._api_node_execution_repo = Mock() + service._api_node_execution_repo.get_execution_by_id.return_value = mock_execution + + result = service._reset_node_var_or_sys_var(workflow, variable) + + # Should succeed and return the variable + assert result == variable + assert variable.last_edited_at is None + mock_session.flush.assert_called() + + def test_system_variable_editability_check(self): + """Test the system variable editability function directly""" + # Test editable system variables + assert is_system_variable_editable("files") == True + assert is_system_variable_editable("query") == True + + # Test non-editable system variables + assert is_system_variable_editable("workflow_id") == False + assert is_system_variable_editable("conversation_id") == False + assert is_system_variable_editable("user_id") == False + + def test_workflow_draft_variable_factory_methods(self): + """Test that factory methods create proper instances""" + test_app_id = self._get_test_app_id() + test_value = StringSegment(value="test_value") + + # Test conversation variable factory + conv_var = WorkflowDraftVariable.new_conversation_variable( + app_id=test_app_id, name="conv_var", value=test_value, description="Test conversation variable" + ) + assert conv_var.get_variable_type() == DraftVariableType.CONVERSATION + assert conv_var.editable == True + assert conv_var.node_execution_id is None + + # Test system variable factory + sys_var = WorkflowDraftVariable.new_sys_variable( + app_id=test_app_id, name="workflow_id", value=test_value, node_execution_id="exec-id", editable=False + ) + assert sys_var.get_variable_type() == DraftVariableType.SYS + assert sys_var.editable == False + assert sys_var.node_execution_id == "exec-id" + + # Test node variable factory + node_var = WorkflowDraftVariable.new_node_variable( + app_id=test_app_id, + node_id="node-id", + name="node_var", + value=test_value, + node_execution_id="exec-id", + visible=True, + editable=True, + ) + assert node_var.get_variable_type() == DraftVariableType.NODE + assert node_var.visible == True + assert node_var.editable == True + assert node_var.node_execution_id == "exec-id" diff --git a/api/tests/unit_tests/services/workflow/test_workflow_node_execution_service_repository.py b/api/tests/unit_tests/services/workflow/test_workflow_node_execution_service_repository.py new file mode 100644 index 0000000000..32d2f8b7e0 --- /dev/null +++ b/api/tests/unit_tests/services/workflow/test_workflow_node_execution_service_repository.py @@ -0,0 +1,288 @@ +from datetime import datetime +from unittest.mock import MagicMock +from uuid import uuid4 + +import pytest +from sqlalchemy.orm import Session + +from models.workflow import WorkflowNodeExecutionModel +from repositories.sqlalchemy_api_workflow_node_execution_repository import ( + DifyAPISQLAlchemyWorkflowNodeExecutionRepository, +) + + +class TestSQLAlchemyWorkflowNodeExecutionServiceRepository: + @pytest.fixture + def repository(self): + mock_session_maker = MagicMock() + return DifyAPISQLAlchemyWorkflowNodeExecutionRepository(session_maker=mock_session_maker) + + @pytest.fixture + def mock_execution(self): + execution = MagicMock(spec=WorkflowNodeExecutionModel) + execution.id = str(uuid4()) + execution.tenant_id = "tenant-123" + execution.app_id = "app-456" + execution.workflow_id = "workflow-789" + execution.workflow_run_id = "run-101" + execution.node_id = "node-202" + execution.index = 1 + execution.created_at = "2023-01-01T00:00:00Z" + return execution + + def test_get_node_last_execution_found(self, repository, mock_execution): + """Test getting the last execution for a node when it exists.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = mock_execution + + # Act + result = repository.get_node_last_execution( + tenant_id="tenant-123", + app_id="app-456", + workflow_id="workflow-789", + node_id="node-202", + ) + + # Assert + assert result == mock_execution + mock_session.scalar.assert_called_once() + # Verify the query was constructed correctly + call_args = mock_session.scalar.call_args[0][0] + assert hasattr(call_args, "compile") # It's a SQLAlchemy statement + + def test_get_node_last_execution_not_found(self, repository): + """Test getting the last execution for a node when it doesn't exist.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + + # Act + result = repository.get_node_last_execution( + tenant_id="tenant-123", + app_id="app-456", + workflow_id="workflow-789", + node_id="node-202", + ) + + # Assert + assert result is None + mock_session.scalar.assert_called_once() + + def test_get_executions_by_workflow_run(self, repository, mock_execution): + """Test getting all executions for a workflow run.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + executions = [mock_execution] + mock_session.execute.return_value.scalars.return_value.all.return_value = executions + + # Act + result = repository.get_executions_by_workflow_run( + tenant_id="tenant-123", + app_id="app-456", + workflow_run_id="run-101", + ) + + # Assert + assert result == executions + mock_session.execute.assert_called_once() + # Verify the query was constructed correctly + call_args = mock_session.execute.call_args[0][0] + assert hasattr(call_args, "compile") # It's a SQLAlchemy statement + + def test_get_executions_by_workflow_run_empty(self, repository): + """Test getting executions for a workflow run when none exist.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + mock_session.execute.return_value.scalars.return_value.all.return_value = [] + + # Act + result = repository.get_executions_by_workflow_run( + tenant_id="tenant-123", + app_id="app-456", + workflow_run_id="run-101", + ) + + # Assert + assert result == [] + mock_session.execute.assert_called_once() + + def test_get_execution_by_id_found(self, repository, mock_execution): + """Test getting execution by ID when it exists.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = mock_execution + + # Act + result = repository.get_execution_by_id(mock_execution.id) + + # Assert + assert result == mock_execution + mock_session.scalar.assert_called_once() + + def test_get_execution_by_id_not_found(self, repository): + """Test getting execution by ID when it doesn't exist.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + + # Act + result = repository.get_execution_by_id("non-existent-id") + + # Assert + assert result is None + mock_session.scalar.assert_called_once() + + def test_repository_implements_protocol(self, repository): + """Test that the repository implements the required protocol methods.""" + # Verify all protocol methods are implemented + assert hasattr(repository, "get_node_last_execution") + assert hasattr(repository, "get_executions_by_workflow_run") + assert hasattr(repository, "get_execution_by_id") + + # Verify methods are callable + assert callable(repository.get_node_last_execution) + assert callable(repository.get_executions_by_workflow_run) + assert callable(repository.get_execution_by_id) + assert callable(repository.delete_expired_executions) + assert callable(repository.delete_executions_by_app) + assert callable(repository.get_expired_executions_batch) + assert callable(repository.delete_executions_by_ids) + + def test_delete_expired_executions(self, repository): + """Test deleting expired executions.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + + # Mock the select query to return some IDs first time, then empty to stop loop + execution_ids = ["id1", "id2"] # Less than batch_size to trigger break + + # Mock execute method to handle both select and delete statements + def mock_execute(stmt): + mock_result = MagicMock() + # For select statements, return execution IDs + if hasattr(stmt, "limit"): # This is our select statement + mock_result.scalars.return_value.all.return_value = execution_ids + else: # This is our delete statement + mock_result.rowcount = 2 + return mock_result + + mock_session.execute.side_effect = mock_execute + + before_date = datetime(2023, 1, 1) + + # Act + result = repository.delete_expired_executions( + tenant_id="tenant-123", + before_date=before_date, + batch_size=1000, + ) + + # Assert + assert result == 2 + assert mock_session.execute.call_count == 2 # One select call, one delete call + mock_session.commit.assert_called_once() + + def test_delete_executions_by_app(self, repository): + """Test deleting executions by app.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + + # Mock the select query to return some IDs first time, then empty to stop loop + execution_ids = ["id1", "id2"] + + # Mock execute method to handle both select and delete statements + def mock_execute(stmt): + mock_result = MagicMock() + # For select statements, return execution IDs + if hasattr(stmt, "limit"): # This is our select statement + mock_result.scalars.return_value.all.return_value = execution_ids + else: # This is our delete statement + mock_result.rowcount = 2 + return mock_result + + mock_session.execute.side_effect = mock_execute + + # Act + result = repository.delete_executions_by_app( + tenant_id="tenant-123", + app_id="app-456", + batch_size=1000, + ) + + # Assert + assert result == 2 + assert mock_session.execute.call_count == 2 # One select call, one delete call + mock_session.commit.assert_called_once() + + def test_get_expired_executions_batch(self, repository): + """Test getting expired executions batch for backup.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + + # Create mock execution objects + mock_execution1 = MagicMock() + mock_execution1.id = "exec-1" + mock_execution2 = MagicMock() + mock_execution2.id = "exec-2" + + mock_session.execute.return_value.scalars.return_value.all.return_value = [mock_execution1, mock_execution2] + + before_date = datetime(2023, 1, 1) + + # Act + result = repository.get_expired_executions_batch( + tenant_id="tenant-123", + before_date=before_date, + batch_size=1000, + ) + + # Assert + assert len(result) == 2 + assert result[0].id == "exec-1" + assert result[1].id == "exec-2" + mock_session.execute.assert_called_once() + + def test_delete_executions_by_ids(self, repository): + """Test deleting executions by IDs.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + + # Mock the delete query result + mock_result = MagicMock() + mock_result.rowcount = 3 + mock_session.execute.return_value = mock_result + + execution_ids = ["id1", "id2", "id3"] + + # Act + result = repository.delete_executions_by_ids(execution_ids) + + # Assert + assert result == 3 + mock_session.execute.assert_called_once() + mock_session.commit.assert_called_once() + + def test_delete_executions_by_ids_empty_list(self, repository): + """Test deleting executions with empty ID list.""" + # Arrange + mock_session = MagicMock(spec=Session) + repository._session_maker.return_value.__enter__.return_value = mock_session + + # Act + result = repository.delete_executions_by_ids([]) + + # Assert + assert result == 0 + mock_session.query.assert_not_called() + mock_session.commit.assert_not_called() diff --git a/api/tests/unit_tests/services/workflow/test_workflow_service.py b/api/tests/unit_tests/services/workflow/test_workflow_service.py index 13393668ea..9700cbaf0e 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_service.py @@ -10,7 +10,8 @@ from services.workflow_service import WorkflowService class TestWorkflowService: @pytest.fixture def workflow_service(self): - return WorkflowService() + mock_session_maker = MagicMock() + return WorkflowService(mock_session_maker) @pytest.fixture def mock_app(self): diff --git a/api/tests/unit_tests/utils/http_parser/test_oauth_convert_request_to_raw_data.py b/api/tests/unit_tests/utils/http_parser/test_oauth_convert_request_to_raw_data.py index f788a9756b..293ac253f5 100644 --- a/api/tests/unit_tests/utils/http_parser/test_oauth_convert_request_to_raw_data.py +++ b/api/tests/unit_tests/utils/http_parser/test_oauth_convert_request_to_raw_data.py @@ -1,3 +1,5 @@ +import json + from werkzeug import Request from werkzeug.datastructures import Headers from werkzeug.test import EnvironBuilder @@ -15,6 +17,59 @@ def test_oauth_convert_request_to_raw_data(): request = Request(builder.get_environ()) raw_request_bytes = oauth_handler._convert_request_to_raw_data(request) - assert b"GET /test HTTP/1.1" in raw_request_bytes + assert b"GET /test? HTTP/1.1" in raw_request_bytes + assert b"Content-Type: application/json" in raw_request_bytes + assert b"\r\n\r\n" in raw_request_bytes + + +def test_oauth_convert_request_to_raw_data_with_query_params(): + oauth_handler = OAuthHandler() + builder = EnvironBuilder( + method="GET", + path="/test", + query_string="code=abc123&state=xyz789", + headers=Headers({"Content-Type": "application/json"}), + ) + request = Request(builder.get_environ()) + raw_request_bytes = oauth_handler._convert_request_to_raw_data(request) + + assert b"GET /test?code=abc123&state=xyz789 HTTP/1.1" in raw_request_bytes + assert b"Content-Type: application/json" in raw_request_bytes + assert b"\r\n\r\n" in raw_request_bytes + + +def test_oauth_convert_request_to_raw_data_with_post_body(): + oauth_handler = OAuthHandler() + builder = EnvironBuilder( + method="POST", + path="/test", + data="param1=value1¶m2=value2", + headers=Headers({"Content-Type": "application/x-www-form-urlencoded"}), + ) + request = Request(builder.get_environ()) + raw_request_bytes = oauth_handler._convert_request_to_raw_data(request) + + assert b"POST /test? HTTP/1.1" in raw_request_bytes + assert b"Content-Type: application/x-www-form-urlencoded" in raw_request_bytes + assert b"\r\n\r\n" in raw_request_bytes + assert b"param1=value1¶m2=value2" in raw_request_bytes + + +def test_oauth_convert_request_to_raw_data_with_json_body(): + oauth_handler = OAuthHandler() + json_data = {"code": "abc123", "state": "xyz789", "grant_type": "authorization_code"} + builder = EnvironBuilder( + method="POST", + path="/test", + data=json.dumps(json_data), + headers=Headers({"Content-Type": "application/json"}), + ) + request = Request(builder.get_environ()) + raw_request_bytes = oauth_handler._convert_request_to_raw_data(request) + + assert b"POST /test? HTTP/1.1" in raw_request_bytes assert b"Content-Type: application/json" in raw_request_bytes assert b"\r\n\r\n" in raw_request_bytes + assert b'"code": "abc123"' in raw_request_bytes + assert b'"state": "xyz789"' in raw_request_bytes + assert b'"grant_type": "authorization_code"' in raw_request_bytes diff --git a/api/tests/unit_tests/utils/structured_output_parser/__init__.py b/api/tests/unit_tests/utils/structured_output_parser/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py new file mode 100644 index 0000000000..93284eed4b --- /dev/null +++ b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py @@ -0,0 +1,465 @@ +from decimal import Decimal +from unittest.mock import MagicMock, patch + +import pytest + +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output +from core.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultChunkWithStructuredOutput, + LLMResultWithStructuredOutput, + LLMUsage, +) +from core.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + SystemPromptMessage, + TextPromptMessageContent, + UserPromptMessage, +) +from core.model_runtime.entities.model_entities import AIModelEntity, ModelType + + +def create_mock_usage(prompt_tokens: int = 10, completion_tokens: int = 5) -> LLMUsage: + """Create a mock LLMUsage with all required fields""" + return LLMUsage( + prompt_tokens=prompt_tokens, + prompt_unit_price=Decimal("0.001"), + prompt_price_unit=Decimal(1), + prompt_price=Decimal(str(prompt_tokens)) * Decimal("0.001"), + completion_tokens=completion_tokens, + completion_unit_price=Decimal("0.002"), + completion_price_unit=Decimal(1), + completion_price=Decimal(str(completion_tokens)) * Decimal("0.002"), + total_tokens=prompt_tokens + completion_tokens, + total_price=Decimal(str(prompt_tokens)) * Decimal("0.001") + Decimal(str(completion_tokens)) * Decimal("0.002"), + currency="USD", + latency=1.5, + ) + + +def get_model_entity(provider: str, model_name: str, support_structure_output: bool = False) -> AIModelEntity: + """Create a mock AIModelEntity for testing""" + model_schema = MagicMock() + model_schema.model = model_name + model_schema.provider = provider + model_schema.model_type = ModelType.LLM + model_schema.model_provider = provider + model_schema.model_name = model_name + model_schema.support_structure_output = support_structure_output + model_schema.parameter_rules = [] + + return model_schema + + +def get_model_instance() -> MagicMock: + """Create a mock ModelInstance for testing""" + mock_instance = MagicMock() + mock_instance.provider = "openai" + mock_instance.credentials = {} + return mock_instance + + +def test_structured_output_parser(): + """Test cases for invoke_llm_with_structured_output function""" + + testcases = [ + # Test case 1: Model with native structured output support, non-streaming + { + "name": "native_structured_output_non_streaming", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": False, + "json_schema": {"type": "object", "properties": {"name": {"type": "string"}}}, + "expected_llm_response": LLMResult( + model="gpt-4o", + message=AssistantPromptMessage(content='{"name": "test"}'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + }, + # Test case 2: Model with native structured output support, streaming + { + "name": "native_structured_output_streaming", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": True, + "json_schema": {"type": "object", "properties": {"name": {"type": "string"}}}, + "expected_llm_response": [ + LLMResultChunk( + model="gpt-4o", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage(content='{"name":'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=2), + ), + ), + LLMResultChunk( + model="gpt-4o", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage(content=' "test"}'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=3), + ), + ), + ], + "expected_result_type": "generator", + "should_raise": False, + }, + # Test case 3: Model without native structured output support, non-streaming + { + "name": "prompt_based_structured_output_non_streaming", + "provider": "anthropic", + "model_name": "claude-3-sonnet", + "support_structure_output": False, + "stream": False, + "json_schema": {"type": "object", "properties": {"answer": {"type": "string"}}}, + "expected_llm_response": LLMResult( + model="claude-3-sonnet", + message=AssistantPromptMessage(content='{"answer": "test response"}'), + usage=create_mock_usage(prompt_tokens=15, completion_tokens=8), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + }, + # Test case 4: Model without native structured output support, streaming + { + "name": "prompt_based_structured_output_streaming", + "provider": "anthropic", + "model_name": "claude-3-sonnet", + "support_structure_output": False, + "stream": True, + "json_schema": {"type": "object", "properties": {"answer": {"type": "string"}}}, + "expected_llm_response": [ + LLMResultChunk( + model="claude-3-sonnet", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage(content='{"answer": "test'), + usage=create_mock_usage(prompt_tokens=15, completion_tokens=3), + ), + ), + LLMResultChunk( + model="claude-3-sonnet", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage(content=' response"}'), + usage=create_mock_usage(prompt_tokens=15, completion_tokens=5), + ), + ), + ], + "expected_result_type": "generator", + "should_raise": False, + }, + # Test case 5: Streaming with list content + { + "name": "streaming_with_list_content", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": True, + "json_schema": {"type": "object", "properties": {"data": {"type": "string"}}}, + "expected_llm_response": [ + LLMResultChunk( + model="gpt-4o", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage( + content=[ + TextPromptMessageContent(data='{"data":'), + ] + ), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=2), + ), + ), + LLMResultChunk( + model="gpt-4o", + prompt_messages=[UserPromptMessage(content="test")], + system_fingerprint="test", + delta=LLMResultChunkDelta( + index=0, + message=AssistantPromptMessage( + content=[ + TextPromptMessageContent(data=' "value"}'), + ] + ), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=3), + ), + ), + ], + "expected_result_type": "generator", + "should_raise": False, + }, + # Test case 6: Error case - non-string LLM response content (non-streaming) + { + "name": "error_non_string_content_non_streaming", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": False, + "json_schema": {"type": "object", "properties": {"name": {"type": "string"}}}, + "expected_llm_response": LLMResult( + model="gpt-4o", + message=AssistantPromptMessage(content=None), # Non-string content + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ), + "expected_result_type": None, + "should_raise": True, + "expected_error": OutputParserError, + }, + # Test case 7: JSON repair scenario + { + "name": "json_repair_scenario", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": False, + "json_schema": {"type": "object", "properties": {"name": {"type": "string"}}}, + "expected_llm_response": LLMResult( + model="gpt-4o", + message=AssistantPromptMessage(content='{"name": "test"'), # Invalid JSON - missing closing brace + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + }, + # Test case 8: Model with parameter rules for response format + { + "name": "model_with_parameter_rules", + "provider": "openai", + "model_name": "gpt-4o", + "support_structure_output": True, + "stream": False, + "json_schema": {"type": "object", "properties": {"result": {"type": "string"}}}, + "parameter_rules": [ + MagicMock(name="response_format", options=["json_schema"], required=False), + ], + "expected_llm_response": LLMResult( + model="gpt-4o", + message=AssistantPromptMessage(content='{"result": "success"}'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + }, + # Test case 9: Model without native support but with JSON response format rules + { + "name": "non_native_with_json_rules", + "provider": "anthropic", + "model_name": "claude-3-sonnet", + "support_structure_output": False, + "stream": False, + "json_schema": {"type": "object", "properties": {"output": {"type": "string"}}}, + "parameter_rules": [ + MagicMock(name="response_format", options=["JSON"], required=False), + ], + "expected_llm_response": LLMResult( + model="claude-3-sonnet", + message=AssistantPromptMessage(content='{"output": "result"}'), + usage=create_mock_usage(prompt_tokens=15, completion_tokens=8), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + }, + ] + + for case in testcases: + print(f"Running test case: {case['name']}") + + # Setup model entity + model_schema = get_model_entity(case["provider"], case["model_name"], case["support_structure_output"]) + + # Add parameter rules if specified + if "parameter_rules" in case: + model_schema.parameter_rules = case["parameter_rules"] + + # Setup model instance + model_instance = get_model_instance() + model_instance.invoke_llm.return_value = case["expected_llm_response"] + + # Setup prompt messages + prompt_messages = [ + SystemPromptMessage(content="You are a helpful assistant."), + UserPromptMessage(content="Generate a response according to the schema."), + ] + + if case["should_raise"]: + # Test error cases + with pytest.raises(case["expected_error"]): # noqa: PT012 + if case["stream"]: + result_generator = invoke_llm_with_structured_output( + provider=case["provider"], + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=case["json_schema"], + stream=case["stream"], + ) + # Consume the generator to trigger the error + list(result_generator) + else: + invoke_llm_with_structured_output( + provider=case["provider"], + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=case["json_schema"], + stream=case["stream"], + ) + else: + # Test successful cases + with patch("core.llm_generator.output_parser.structured_output.json_repair.loads") as mock_json_repair: + # Configure json_repair mock for cases that need it + if case["name"] == "json_repair_scenario": + mock_json_repair.return_value = {"name": "test"} + + result = invoke_llm_with_structured_output( + provider=case["provider"], + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=case["json_schema"], + stream=case["stream"], + model_parameters={"temperature": 0.7, "max_tokens": 100}, + user="test_user", + ) + + if case["expected_result_type"] == "generator": + # Test streaming results + assert hasattr(result, "__iter__") + chunks = list(result) + assert len(chunks) > 0 + + # Verify all chunks are LLMResultChunkWithStructuredOutput + for chunk in chunks[:-1]: # All except last + assert isinstance(chunk, LLMResultChunkWithStructuredOutput) + assert chunk.model == case["model_name"] + + # Last chunk should have structured output + last_chunk = chunks[-1] + assert isinstance(last_chunk, LLMResultChunkWithStructuredOutput) + assert last_chunk.structured_output is not None + assert isinstance(last_chunk.structured_output, dict) + else: + # Test non-streaming results + assert isinstance(result, case["expected_result_type"]) + assert result.model == case["model_name"] + assert result.structured_output is not None + assert isinstance(result.structured_output, dict) + + # Verify model_instance.invoke_llm was called with correct parameters + model_instance.invoke_llm.assert_called_once() + call_args = model_instance.invoke_llm.call_args + + assert call_args.kwargs["stream"] == case["stream"] + assert call_args.kwargs["user"] == "test_user" + assert "temperature" in call_args.kwargs["model_parameters"] + assert "max_tokens" in call_args.kwargs["model_parameters"] + + +def test_parse_structured_output_edge_cases(): + """Test edge cases for structured output parsing""" + + # Test case with list that contains dict (reasoning model scenario) + testcase_list_with_dict = { + "name": "list_with_dict_parsing", + "provider": "deepseek", + "model_name": "deepseek-r1", + "support_structure_output": False, + "stream": False, + "json_schema": {"type": "object", "properties": {"thought": {"type": "string"}}}, + "expected_llm_response": LLMResult( + model="deepseek-r1", + message=AssistantPromptMessage(content='[{"thought": "reasoning process"}, "other content"]'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ), + "expected_result_type": LLMResultWithStructuredOutput, + "should_raise": False, + } + + # Setup for list parsing test + model_schema = get_model_entity( + testcase_list_with_dict["provider"], + testcase_list_with_dict["model_name"], + testcase_list_with_dict["support_structure_output"], + ) + + model_instance = get_model_instance() + model_instance.invoke_llm.return_value = testcase_list_with_dict["expected_llm_response"] + + prompt_messages = [UserPromptMessage(content="Test reasoning")] + + with patch("core.llm_generator.output_parser.structured_output.json_repair.loads") as mock_json_repair: + # Mock json_repair to return a list with dict + mock_json_repair.return_value = [{"thought": "reasoning process"}, "other content"] + + result = invoke_llm_with_structured_output( + provider=testcase_list_with_dict["provider"], + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=testcase_list_with_dict["json_schema"], + stream=testcase_list_with_dict["stream"], + ) + + assert isinstance(result, LLMResultWithStructuredOutput) + assert result.structured_output == {"thought": "reasoning process"} + + +def test_model_specific_schema_preparation(): + """Test schema preparation for different model types""" + + # Test Gemini model + gemini_case = { + "provider": "google", + "model_name": "gemini-pro", + "support_structure_output": True, + "stream": False, + "json_schema": {"type": "object", "properties": {"result": {"type": "boolean"}}, "additionalProperties": False}, + } + + model_schema = get_model_entity( + gemini_case["provider"], gemini_case["model_name"], gemini_case["support_structure_output"] + ) + + model_instance = get_model_instance() + model_instance.invoke_llm.return_value = LLMResult( + model="gemini-pro", + message=AssistantPromptMessage(content='{"result": "true"}'), + usage=create_mock_usage(prompt_tokens=10, completion_tokens=5), + ) + + prompt_messages = [UserPromptMessage(content="Test")] + + result = invoke_llm_with_structured_output( + provider=gemini_case["provider"], + model_schema=model_schema, + model_instance=model_instance, + prompt_messages=prompt_messages, + json_schema=gemini_case["json_schema"], + stream=gemini_case["stream"], + ) + + assert isinstance(result, LLMResultWithStructuredOutput) + + # Verify model_instance.invoke_llm was called and check the schema preparation + model_instance.invoke_llm.assert_called_once() + call_args = model_instance.invoke_llm.call_args + + # For Gemini, the schema should not have additionalProperties and boolean should be converted to string + assert "json_schema" in call_args.kwargs["model_parameters"] diff --git a/api/uv.lock b/api/uv.lock index 1f1ba66de7..21b6b20f53 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version >= '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", @@ -20,23 +20,23 @@ resolution-markers = [ name = "aiofiles" version = "24.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] name = "aiohttp" -version = "3.12.7" +version = "3.12.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -47,42 +47,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/62/95588e933dfea06a3af0332990bd19f6768f8f37fa4c0fe33fe4c55cf9d0/aiohttp-3.12.7.tar.gz", hash = "sha256:08bf55b216c779eddb6e41c1841c17d7ddd12776c7d7b36051c0a292a9ca828e", size = 7806530 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/19/37560cc111d6fd95ff6c4bd14445e3c629269fce406c89cc7a69a2865ecf/aiohttp-3.12.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:388b5947aa6931ef4ce3ed4edde6853e84980677886992cfadcf733dd06eed63", size = 707169 }, - { url = "https://files.pythonhosted.org/packages/b9/18/29bbefb094f81a687473c1d31391bf8a4c48c7b5b8559c3679fc14e67597/aiohttp-3.12.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ed5af1cce257cca27a3e920b003b3b397f63418a203064b7d804ea3b45782af", size = 479443 }, - { url = "https://files.pythonhosted.org/packages/cf/7d/119f3e012c75a3fe38f86ac1d77f1452779e0e940770d5827d4e62aa5655/aiohttp-3.12.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f466ae8f9c02993b7d167be685bdbeb527cf254a3cfcc757697e0e336399d0a2", size = 467706 }, - { url = "https://files.pythonhosted.org/packages/83/f1/f61d8573d648e17347ab9a112063e4363664b5b6100792467fbb26028bde/aiohttp-3.12.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2be095a420a9f9a12eff343d877ae180dd919238b539431af08cef929e874759", size = 1737902 }, - { url = "https://files.pythonhosted.org/packages/4b/f8/7a8a000bc63de3c79aaa8f03b0784e29e9982276f4579c5e2e56d560e403/aiohttp-3.12.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b058cf2ba6adba699960d7bc403411c8a99ab5d3e5ea3eb01473638ae7d1a30e", size = 1686569 }, - { url = "https://files.pythonhosted.org/packages/5c/4e/29a5b35ca9a598f51dc7deff4e8403bf813988f30a8b250e25a8442641b7/aiohttp-3.12.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b6a660163b055686dbb0acc961978fd14537eba5d9da6cbdb4dced7a8d3be1a", size = 1785359 }, - { url = "https://files.pythonhosted.org/packages/f9/36/0521398a69c40ac24c659b130597e2544cde1d7dd00291b8a6206bb552d0/aiohttp-3.12.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d741923905f267ad5d5c8f86a56f9d2beac9f32a36c217c5d9ef65cd74fd8ca0", size = 1824408 }, - { url = "https://files.pythonhosted.org/packages/c1/41/79506d76da96399b6b700acbe10b14291547a3b49a1cc7ed2c5edaa199ce/aiohttp-3.12.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:519f5454b6018158ae0e789b8f6a88726c47dd680982eb318ef3ca4dee727314", size = 1726867 }, - { url = "https://files.pythonhosted.org/packages/32/d1/d59ed16962934b46c7569d04af2dc9638a38ae5812680b9d6c7ee42d770e/aiohttp-3.12.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4eebfe470e22cc4b374d7e32c07e96d777a5c0fa51f3824de68e697da037ec", size = 1663943 }, - { url = "https://files.pythonhosted.org/packages/15/d5/971d1b277e6a3d5b679f0c9ba076c343a5125ea2eacc51c23ea7d875d43a/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:74ff39445f94923cf595e9e6dd602ecbe66b12364e2207e61342b8834417f8da", size = 1712217 }, - { url = "https://files.pythonhosted.org/packages/e1/9c/c21fd0ba87772f3d1d43cdbfcfd40fe29f37d36a5d73997a8a4d4d1485c3/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:77cb9dba16486ecfeac8076763600b9714941e0ff696e53a30e8d408d9a196ca", size = 1707375 }, - { url = "https://files.pythonhosted.org/packages/85/48/bb97ef3a694df852b70e6f5c1aaf3621a3a26b35f0a0d90481f3fdb1ce8b/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a7b3b9cbe83e3918a1918b0de274884f17b64224c1c9210a6fb0f7c10d246636", size = 1687561 }, - { url = "https://files.pythonhosted.org/packages/d5/75/0b85f30ba9eb1dbdb5d3a53d3a0db29990220f69187acb24d06903686c5d/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6055f53c70938498884e71ca966abe8e9e7558489e13a7e40b6384dee7230d1d", size = 1781163 }, - { url = "https://files.pythonhosted.org/packages/92/51/6350a4c485c7d2fb794101d46085c3830485ec1c37738d8af6c9c5ed8e1a/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8493a42d5b2a736c6804239b985feebeea1c60f8fcb46a3607d6dce3c1a42b12", size = 1801624 }, - { url = "https://files.pythonhosted.org/packages/4f/dd/6a75eaaac93b5552090e42c38a576062028ce4af50f0b50ac550332d332c/aiohttp-3.12.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f2237cade45f563d973c2a913895f2699a892c0eb11c55c6880b6f0acf219", size = 1714679 }, - { url = "https://files.pythonhosted.org/packages/de/ad/0574964387d8eed7fcd0c2fe6ef20c4affe0b265c710938d5fffdb3776b5/aiohttp-3.12.7-cp311-cp311-win32.whl", hash = "sha256:41f686749a099b507563a5c0cb4fd77367b05448a2c1758784ad506a28e9e579", size = 424709 }, - { url = "https://files.pythonhosted.org/packages/4f/ab/6b82b43abb0990e4452aaef509cf1403ab50c04b5c090f92fb1b255fb319/aiohttp-3.12.7-cp311-cp311-win_amd64.whl", hash = "sha256:7a3691583470d4397aca70fbf8e0f0778b63a2c2a6a23263bdeeb68395972f29", size = 449100 }, - { url = "https://files.pythonhosted.org/packages/5d/65/0bd8ccbffa33ee69db9f5c43f3f62fb8b600b607388e9a8deab8962d0523/aiohttp-3.12.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b9345918f5b5156a5712c37d1d331baf320df67547ea032a49a609b773c3606", size = 698263 }, - { url = "https://files.pythonhosted.org/packages/99/64/a48a8abc4e684fb447d1f7b61e7adcb19865b91e20b50595f49b2942fbb3/aiohttp-3.12.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3091b4883f405dbabeb9ea821a25dec16d03a51c3e0d2752fc3ab48b652bf196", size = 472877 }, - { url = "https://files.pythonhosted.org/packages/7d/e4/994bc56a7d7733e9cd1f45db8b656e78d51d7a61cefff8043ec4f7d4a23f/aiohttp-3.12.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97fd97abd4cf199eff4041d0346a7dc68b60deab177f01de87283be513ffc3ab", size = 465716 }, - { url = "https://files.pythonhosted.org/packages/39/b0/bddc489288a0e3b05fa05387db9caebc38577204a17db0d5428abae524ba/aiohttp-3.12.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a5938973105cd5ff17176e8cb36bc19cac7c82ae7c58c0dbd7e023972d0c708", size = 1712513 }, - { url = "https://files.pythonhosted.org/packages/4d/4a/c06d3ce0dc5f96338cc8d18da57d74608585a3751234eeef5952e4f48ade/aiohttp-3.12.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e506ae5c4c05d1a1e87edd64b994cea2d49385d41d32e1c6be8764f31cf2245c", size = 1695167 }, - { url = "https://files.pythonhosted.org/packages/79/ec/e847fdfe2b1c1f1a2b0ba5343a9b2bd033a0545f8eaf1f7894a6614473ae/aiohttp-3.12.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b780b402e6361c4cfcec252580f5ecdd86cb68376520ac34748d3f8b262dd598", size = 1750261 }, - { url = "https://files.pythonhosted.org/packages/2c/5e/b832ff59737d99cc5ae51b737c52976d19990ccee922ba6fe811f615e7f9/aiohttp-3.12.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf981bbfb7ff2ebc1b3bfae49d2efe2c51ca1cf3d90867f47c310df65398e85e", size = 1796416 }, - { url = "https://files.pythonhosted.org/packages/e0/ff/51ae87efce9b53aafd384179f58923bf178f561897cf80054a440fdf8363/aiohttp-3.12.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f98e0e5a49f89b252e115844f756c04fc8050f38252a32a3dd994ce8121f10", size = 1715855 }, - { url = "https://files.pythonhosted.org/packages/b1/54/5a77116498f84d2503f5588e687eccfa43a85aa2450bc195ee6e5bb75695/aiohttp-3.12.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:410e96cc6824fc4ced9703fb2ac2d06c6190d21fc6f5b588f62b1918628449c1", size = 1631656 }, - { url = "https://files.pythonhosted.org/packages/46/34/554220592f8ade7f3cabebfb9325e95078f842140f293ced3ab977fd13ec/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e93987fe9df4349db8deae7c391695538c35e4ba893133c7e823234f6e4537", size = 1692718 }, - { url = "https://files.pythonhosted.org/packages/ff/9d/ae7103bb8c73c3521e38ae8cde301ddc937024b1681ce134bb1ef01be7d0/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb3f3dcb59f3e16819a1c7d3fa32e7b87255b661c1e139a1b5940bde270704ab", size = 1714171 }, - { url = "https://files.pythonhosted.org/packages/5d/4d/9b8b8f362e36392939019340321f7efcc1807bb2c4cdea8eb1019d3398ff/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4a46fe4a4c66b2712059e48a8384eb93565fbe3251af4844860fed846ef4ca75", size = 1654822 }, - { url = "https://files.pythonhosted.org/packages/48/30/0ca82df423ee346206bc167852c825cd210c11d2f1fa0064a2a55d7f60d5/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ad01793164661af70918490ef8efc2c09df7a3c686b6c84ca90a2d69cdbc3911", size = 1734385 }, - { url = "https://files.pythonhosted.org/packages/43/bd/96d12318c0f82ac8323bd4459ee26291ad220f688988077a21e538b0872c/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e85c6833be3f49cead2e7bc79080e5c18d6dab9af32226ab5a01dc20c523e7d9", size = 1762356 }, - { url = "https://files.pythonhosted.org/packages/6c/39/7a9b706bf42f293415584d60cf35e80d0558929ab70e72cb40b747f0dfc7/aiohttp-3.12.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c9f52149d8249566e72c50c7985c2345521b3b78f84aa86f6f492cd50b14793", size = 1721970 }, - { url = "https://files.pythonhosted.org/packages/19/f2/8899367a52dec8100f43036e5a792cfdbae317bf3a80549da90290083ff4/aiohttp-3.12.7-cp312-cp312-win32.whl", hash = "sha256:0e1c33ac0f6a396bcefe9c1d52c9d38a051861885a5c102ca5c8298aba0108fa", size = 419443 }, - { url = "https://files.pythonhosted.org/packages/e8/34/ad5225b4edbcc23496537011d67ef1a147c03205c07340f4a50993b219b9/aiohttp-3.12.7-cp312-cp312-win_amd64.whl", hash = "sha256:b4aed5233a9d13e34e8624ecb798533aa2da97e7048cc69671b7a6d7a2efe7e8", size = 445544 }, +sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401, upload-time = "2025-06-14T15:13:30.774Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669, upload-time = "2025-06-14T15:13:32.316Z" }, + { url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933, upload-time = "2025-06-14T15:13:34.104Z" }, + { url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128, upload-time = "2025-06-14T15:13:35.604Z" }, + { url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796, upload-time = "2025-06-14T15:13:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589, upload-time = "2025-06-14T15:13:38.745Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635, upload-time = "2025-06-14T15:13:40.733Z" }, + { url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095, upload-time = "2025-06-14T15:13:42.312Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170, upload-time = "2025-06-14T15:13:44.884Z" }, + { url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444, upload-time = "2025-06-14T15:13:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604, upload-time = "2025-06-14T15:13:48.377Z" }, + { url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786, upload-time = "2025-06-14T15:13:50.401Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389, upload-time = "2025-06-14T15:13:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853, upload-time = "2025-06-14T15:13:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909, upload-time = "2025-06-14T15:13:55.148Z" }, + { url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036, upload-time = "2025-06-14T15:13:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427, upload-time = "2025-06-14T15:13:58.505Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, + { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, ] [[package]] @@ -92,35 +92,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymysql" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/76/2c5b55e4406a1957ffdfd933a94c2517455291c97d2b81cec6813754791a/aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67", size = 114706 } +sdist = { url = "https://files.pythonhosted.org/packages/67/76/2c5b55e4406a1957ffdfd933a94c2517455291c97d2b81cec6813754791a/aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67", size = 114706, upload-time = "2023-06-11T19:57:53.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/87/c982ee8b333c85b8ae16306387d703a1fcdfc81a2f3f15a24820ab1a512d/aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a", size = 44215 }, + { url = "https://files.pythonhosted.org/packages/42/87/c982ee8b333c85b8ae16306387d703a1fcdfc81a2f3f15a24820ab1a512d/aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a", size = 44215, upload-time = "2023-06-11T19:57:51.09Z" }, ] [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424 } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597 }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "alembic" -version = "1.16.1" +version = "1.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/89/bfb4fe86e3fc3972d35431af7bedbc60fa606e8b17196704a1747f7aa4c3/alembic-1.16.1.tar.gz", hash = "sha256:43d37ba24b3d17bc1eb1024fe0f51cd1dc95aeb5464594a02c6bb9ca9864bfa4", size = 1955006 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/40/28683414cc8711035a65256ca689e159471aa9ef08e8741ad1605bc01066/alembic-1.16.3.tar.gz", hash = "sha256:18ad13c1f40a5796deee4b2346d1a9c382f44b8af98053897484fa6cf88025e4", size = 1967462, upload-time = "2025-07-08T18:57:50.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/59/565286efff3692c5716c212202af61466480f6357c4ae3089d4453bff1f3/alembic-1.16.1-py3-none-any.whl", hash = "sha256:0cdd48acada30d93aa1035767d67dff25702f8de74d7c3919f2e8492c8db2e67", size = 242488 }, + { url = "https://files.pythonhosted.org/packages/e6/68/1dea77887af7304528ea944c355d769a7ccc4599d3a23bd39182486deb42/alembic-1.16.3-py3-none-any.whl", hash = "sha256:70a7c7829b792de52d08ca0e3aefaf060687cb8ed6bebfa557e597a1a5e5a481", size = 246933, upload-time = "2025-07-08T18:57:52.793Z" }, ] [[package]] @@ -133,22 +134,19 @@ dependencies = [ { name = "alibabacloud-tea" }, { name = "apscheduler" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/0c/1b0c5f4c2170165719b336616ac0a88f1666fd8690fda41e2e8ae3139fd9/alibabacloud-credentials-1.0.2.tar.gz", hash = "sha256:d2368eb70bd02db9143b2bf531a27a6fecd2cde9601db6e5b48cd6dbe25720ce", size = 30804 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/0c/1b0c5f4c2170165719b336616ac0a88f1666fd8690fda41e2e8ae3139fd9/alibabacloud-credentials-1.0.2.tar.gz", hash = "sha256:d2368eb70bd02db9143b2bf531a27a6fecd2cde9601db6e5b48cd6dbe25720ce", size = 30804, upload-time = "2025-05-06T12:30:35.46Z" } [[package]] name = "alibabacloud-credentials-api" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/87/1d7019d23891897cb076b2f7e3c81ab3c2ba91de3bb067196f675d60d34c/alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f", size = 2330 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/87/1d7019d23891897cb076b2f7e3c81ab3c2ba91de3bb067196f675d60d34c/alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f", size = 2330, upload-time = "2025-01-13T05:53:04.931Z" } [[package]] name = "alibabacloud-endpoint-util" -version = "0.0.3" +version = "0.0.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alibabacloud-tea" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/54/56736a0f224b3f2b65bb9c9ff9e20fa390351a7587c99f19ca1f8d596ae1/alibabacloud_endpoint_util-0.0.3.tar.gz", hash = "sha256:8c0efb76fdcc3af4ca716ef24bbce770201a3f83f98c0afcf81655f684b9c7d2", size = 2756 } +sdist = { url = "https://files.pythonhosted.org/packages/92/7d/8cc92a95c920e344835b005af6ea45a0db98763ad6ad19299d26892e6c8d/alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90", size = 2813, upload-time = "2025-06-12T07:20:52.572Z" } [[package]] name = "alibabacloud-gateway-spi" @@ -157,7 +155,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-credentials" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/98/d7111245f17935bf72ee9bea60bbbeff2bc42cdfe24d2544db52bc517e1a/alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b", size = 4249 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/98/d7111245f17935bf72ee9bea60bbbeff2bc42cdfe24d2544db52bc517e1a/alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b", size = 4249, upload-time = "2025-02-23T16:29:54.222Z" } [[package]] name = "alibabacloud-gpdb20160503" @@ -173,9 +171,9 @@ dependencies = [ { name = "alibabacloud-tea-openapi" }, { name = "alibabacloud-tea-util" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/6a/cc72e744e95c8f37fa6a84e66ae0b9b57a13ee97a0ef03d94c7127c31d75/alibabacloud_gpdb20160503-3.8.3.tar.gz", hash = "sha256:4dfcc0d9cff5a921d529d76f4bf97e2ceb9dc2fa53f00ab055f08509423d8e30", size = 155092 } +sdist = { url = "https://files.pythonhosted.org/packages/15/6a/cc72e744e95c8f37fa6a84e66ae0b9b57a13ee97a0ef03d94c7127c31d75/alibabacloud_gpdb20160503-3.8.3.tar.gz", hash = "sha256:4dfcc0d9cff5a921d529d76f4bf97e2ceb9dc2fa53f00ab055f08509423d8e30", size = 155092, upload-time = "2024-07-18T17:09:42.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/36/bce41704b3bf59d607590ec73a42a254c5dea27c0f707aee11d20512a200/alibabacloud_gpdb20160503-3.8.3-py3-none-any.whl", hash = "sha256:06e1c46ce5e4e9d1bcae76e76e51034196c625799d06b2efec8d46a7df323fe8", size = 156097 }, + { url = "https://files.pythonhosted.org/packages/ab/36/bce41704b3bf59d607590ec73a42a254c5dea27c0f707aee11d20512a200/alibabacloud_gpdb20160503-3.8.3-py3-none-any.whl", hash = "sha256:06e1c46ce5e4e9d1bcae76e76e51034196c625799d06b2efec8d46a7df323fe8", size = 156097, upload-time = "2024-07-18T17:09:40.414Z" }, ] [[package]] @@ -186,7 +184,7 @@ dependencies = [ { name = "alibabacloud-tea-util" }, { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/50/5f41ab550d7874c623f6e992758429802c4b52a6804db437017e5387de33/alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8", size = 7201 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/50/5f41ab550d7874c623f6e992758429802c4b52a6804db437017e5387de33/alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8", size = 7201, upload-time = "2023-10-23T07:44:18.523Z" } [[package]] name = "alibabacloud-openplatform20191219" @@ -198,9 +196,9 @@ dependencies = [ { name = "alibabacloud-tea-openapi" }, { name = "alibabacloud-tea-util" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4f/bf/f7fa2f3657ed352870f442434cb2f27b7f70dcd52a544a1f3998eeaf6d71/alibabacloud_openplatform20191219-2.0.0.tar.gz", hash = "sha256:e67f4c337b7542538746592c6a474bd4ae3a9edccdf62e11a32ca61fad3c9020", size = 5038 } +sdist = { url = "https://files.pythonhosted.org/packages/4f/bf/f7fa2f3657ed352870f442434cb2f27b7f70dcd52a544a1f3998eeaf6d71/alibabacloud_openplatform20191219-2.0.0.tar.gz", hash = "sha256:e67f4c337b7542538746592c6a474bd4ae3a9edccdf62e11a32ca61fad3c9020", size = 5038, upload-time = "2022-09-21T06:16:10.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/e5/18c75213551eeca9db1f6b41ddcc0bd87b5b6508c75a67f05cd8671847b4/alibabacloud_openplatform20191219-2.0.0-py3-none-any.whl", hash = "sha256:873821c45bca72a6c6ec7a906c9cb21554c122e88893bbac3986934dab30dd36", size = 5204 }, + { url = "https://files.pythonhosted.org/packages/94/e5/18c75213551eeca9db1f6b41ddcc0bd87b5b6508c75a67f05cd8671847b4/alibabacloud_openplatform20191219-2.0.0-py3-none-any.whl", hash = "sha256:873821c45bca72a6c6ec7a906c9cb21554c122e88893bbac3986934dab30dd36", size = 5204, upload-time = "2022-09-21T06:16:07.844Z" }, ] [[package]] @@ -214,7 +212,7 @@ dependencies = [ { name = "alibabacloud-tea-util" }, { name = "alibabacloud-tea-xml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/d1/f442dd026908fcf55340ca694bb1d027aa91e119e76ae2fbea62f2bde4f4/alibabacloud_oss_sdk-0.1.1.tar.gz", hash = "sha256:f51a368020d0964fcc0978f96736006f49f5ab6a4a4bf4f0b8549e2c659e7358", size = 46434 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/d1/f442dd026908fcf55340ca694bb1d027aa91e119e76ae2fbea62f2bde4f4/alibabacloud_oss_sdk-0.1.1.tar.gz", hash = "sha256:f51a368020d0964fcc0978f96736006f49f5ab6a4a4bf4f0b8549e2c659e7358", size = 46434, upload-time = "2025-04-22T12:40:41.717Z" } [[package]] name = "alibabacloud-oss-util" @@ -223,7 +221,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-tea" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/7c/d7e812b9968247a302573daebcfef95d0f9a718f7b4bfcca8d3d83e266be/alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6", size = 10008 } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/d7e812b9968247a302573daebcfef95d0f9a718f7b4bfcca8d3d83e266be/alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6", size = 10008, upload-time = "2021-04-28T09:25:04.056Z" } [[package]] name = "alibabacloud-tea" @@ -233,7 +231,7 @@ dependencies = [ { name = "aiohttp" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0f3f9d7f26b76b9ed93d4101add7867a2c87ed2534f5/alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a", size = 8785 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0f3f9d7f26b76b9ed93d4101add7867a2c87ed2534f5/alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a", size = 8785, upload-time = "2025-03-24T07:34:42.958Z" } [[package]] name = "alibabacloud-tea-fileform" @@ -242,11 +240,11 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-tea" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/8a/ef8ddf5ee0350984cad2749414b420369fe943e15e6d96b79be45367630e/alibabacloud_tea_fileform-0.0.5.tar.gz", hash = "sha256:fd00a8c9d85e785a7655059e9651f9e91784678881831f60589172387b968ee8", size = 3961 } +sdist = { url = "https://files.pythonhosted.org/packages/22/8a/ef8ddf5ee0350984cad2749414b420369fe943e15e6d96b79be45367630e/alibabacloud_tea_fileform-0.0.5.tar.gz", hash = "sha256:fd00a8c9d85e785a7655059e9651f9e91784678881831f60589172387b968ee8", size = 3961, upload-time = "2021-04-28T09:22:54.56Z" } [[package]] name = "alibabacloud-tea-openapi" -version = "0.3.15" +version = "0.3.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-credentials" }, @@ -255,7 +253,7 @@ dependencies = [ { name = "alibabacloud-tea-util" }, { name = "alibabacloud-tea-xml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/cb/f1b10b1da37e4c0de2aa9ca1e7153a6960a7f2dc496664e85fdc8b621f84/alibabacloud_tea_openapi-0.3.15.tar.gz", hash = "sha256:56a0aa6d51d8cf18c0cf3d219d861f4697f59d3e17fa6726b1101826d93988a2", size = 13021 } +sdist = { url = "https://files.pythonhosted.org/packages/09/be/f594e79625e5ccfcfe7f12d7d70709a3c59e920878469c998886211c850d/alibabacloud_tea_openapi-0.3.16.tar.gz", hash = "sha256:6bffed8278597592e67860156f424bde4173a6599d7b6039fb640a3612bae292", size = 13087, upload-time = "2025-07-04T09:30:10.689Z" } [[package]] name = "alibabacloud-tea-util" @@ -264,16 +262,16 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-tea" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/18/35be17103c8f40f9eebec3b1567f51b3eec09c3a47a5dd62bcb413f4e619/alibabacloud_tea_util-0.3.13.tar.gz", hash = "sha256:8cbdfd2a03fbbf622f901439fa08643898290dd40e1d928347f6346e43f63c90", size = 6535 } +sdist = { url = "https://files.pythonhosted.org/packages/23/18/35be17103c8f40f9eebec3b1567f51b3eec09c3a47a5dd62bcb413f4e619/alibabacloud_tea_util-0.3.13.tar.gz", hash = "sha256:8cbdfd2a03fbbf622f901439fa08643898290dd40e1d928347f6346e43f63c90", size = 6535, upload-time = "2024-07-15T12:25:12.07Z" } [[package]] name = "alibabacloud-tea-xml" -version = "0.0.2" +version = "0.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alibabacloud-tea" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/30/f934051b1f65525d450cb225e4ac81dc3b77d808b0f79d51059d2a7ad3d3/alibabacloud_tea_xml-0.0.2.tar.gz", hash = "sha256:f0135e8148fd7d9c1f029db161863f37f144f837c280cba16c2edeb2f9c549d8", size = 3378 } +sdist = { url = "https://files.pythonhosted.org/packages/32/eb/5e82e419c3061823f3feae9b5681588762929dc4da0176667297c2784c1a/alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c", size = 3466, upload-time = "2025-07-01T08:04:55.144Z" } [[package]] name = "aliyun-python-sdk-core" @@ -283,7 +281,7 @@ dependencies = [ { name = "cryptography" }, { name = "jmespath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" } [[package]] name = "aliyun-python-sdk-kms" @@ -292,9 +290,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aliyun-python-sdk-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495 }, + { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, ] [[package]] @@ -304,27 +302,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013 } +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944 }, + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] [[package]] name = "aniso8601" version = "10.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/52179c4e3f1978d3d9a285f98c706642522750ef343e9738286130423730/aniso8601-10.0.1.tar.gz", hash = "sha256:25488f8663dd1528ae1f54f94ac1ea51ae25b4d531539b8bc707fed184d16845", size = 47190 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/52179c4e3f1978d3d9a285f98c706642522750ef343e9738286130423730/aniso8601-10.0.1.tar.gz", hash = "sha256:25488f8663dd1528ae1f54f94ac1ea51ae25b4d531539b8bc707fed184d16845", size = 47190, upload-time = "2025-04-18T17:29:42.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/75/e0e10dc7ed1408c28e03a6cb2d7a407f99320eb953f229d008a7a6d05546/aniso8601-10.0.1-py2.py3-none-any.whl", hash = "sha256:eb19717fd4e0db6de1aab06f12450ab92144246b257423fe020af5748c0cb89e", size = 52848 }, + { url = "https://files.pythonhosted.org/packages/59/75/e0e10dc7ed1408c28e03a6cb2d7a407f99320eb953f229d008a7a6d05546/aniso8601-10.0.1-py2.py3-none-any.whl", hash = "sha256:eb19717fd4e0db6de1aab06f12450ab92144246b257423fe020af5748c0cb89e", size = 52848, upload-time = "2025-04-18T17:29:41.492Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -336,9 +334,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[package]] @@ -348,36 +346,54 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzlocal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004 }, + { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" }, +] + +[[package]] +name = "arize-phoenix-otel" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openinference-instrumentation" }, + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/b9/8c89191eb46915e9ba7bdb473e2fb1c510b7db3635ae5ede5e65b2176b9d/arize_phoenix_otel-0.9.2.tar.gz", hash = "sha256:a48c7d41f3ac60dc75b037f036bf3306d2af4af371cdb55e247e67957749bc31", size = 11599, upload-time = "2025-04-14T22:05:28.637Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/3d/f64136a758c649e883315939f30fe51ad0747024b0db05fd78450801a78d/arize_phoenix_otel-0.9.2-py3-none-any.whl", hash = "sha256:5286b33c58b596ef8edd9a4255ee00fd74f774b1e5dbd9393e77e87870a14d76", size = 12560, upload-time = "2025-04-14T22:05:27.162Z" }, ] [[package]] name = "asgiref" -version = "3.8.1" +version = "3.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 }, + { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] @@ -387,23 +403,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074 } +sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074, upload-time = "2024-06-04T14:15:32.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 }, + { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827, upload-time = "2024-06-04T14:15:29.218Z" }, ] [[package]] name = "azure-core" -version = "1.34.0" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999 } +sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409 }, + { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, ] [[package]] @@ -416,9 +432,9 @@ dependencies = [ { name = "msal" }, { name = "msal-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/1c/bd704075e555046e24b069157ca25c81aedb4199c3e0b35acba9243a6ca6/azure-identity-1.16.1.tar.gz", hash = "sha256:6d93f04468f240d59246d8afde3091494a5040d4f141cad0f49fc0c399d0d91e", size = 236726 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/1c/bd704075e555046e24b069157ca25c81aedb4199c3e0b35acba9243a6ca6/azure-identity-1.16.1.tar.gz", hash = "sha256:6d93f04468f240d59246d8afde3091494a5040d4f141cad0f49fc0c399d0d91e", size = 236726, upload-time = "2024-06-10T22:23:27.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c5/ca55106564d2044ab90614381368b3756690fb7e3ab04552e17f308e4e4f/azure_identity-1.16.1-py3-none-any.whl", hash = "sha256:8fb07c25642cd4ac422559a8b50d3e77f73dcc2bbfaba419d06d6c9d7cff6726", size = 166741 }, + { url = "https://files.pythonhosted.org/packages/ef/c5/ca55106564d2044ab90614381368b3756690fb7e3ab04552e17f308e4e4f/azure_identity-1.16.1-py3-none-any.whl", hash = "sha256:8fb07c25642cd4ac422559a8b50d3e77f73dcc2bbfaba419d06d6c9d7cff6726", size = 166741, upload-time = "2024-06-10T22:23:30.906Z" }, ] [[package]] @@ -430,18 +446,18 @@ dependencies = [ { name = "cryptography" }, { name = "msrest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/93/b13bf390e940a79a399981f75ac8d2e05a70112a95ebb7b41e9b752d2921/azure-storage-blob-12.13.0.zip", hash = "sha256:53f0d4cd32970ac9ff9b9753f83dd2fb3f9ac30e1d01e71638c436c509bfd884", size = 684838 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/93/b13bf390e940a79a399981f75ac8d2e05a70112a95ebb7b41e9b752d2921/azure-storage-blob-12.13.0.zip", hash = "sha256:53f0d4cd32970ac9ff9b9753f83dd2fb3f9ac30e1d01e71638c436c509bfd884", size = 684838, upload-time = "2022-07-07T22:35:44.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/2a/b8246df35af68d64fb7292c93dbbde63cd25036f2f669a9d9ae59e518c76/azure_storage_blob-12.13.0-py3-none-any.whl", hash = "sha256:280a6ab032845bab9627582bee78a50497ca2f14772929b5c5ee8b4605af0cb3", size = 377309 }, + { url = "https://files.pythonhosted.org/packages/0e/2a/b8246df35af68d64fb7292c93dbbde63cd25036f2f669a9d9ae59e518c76/azure_storage_blob-12.13.0-py3-none-any.whl", hash = "sha256:280a6ab032845bab9627582bee78a50497ca2f14772929b5c5ee8b4605af0cb3", size = 377309, upload-time = "2022-07-07T22:35:41.905Z" }, ] [[package]] name = "backoff" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] [[package]] @@ -453,49 +469,49 @@ dependencies = [ { name = "pycryptodome" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/91/c218750fd515fef10d197a2385a81a5f3504d30637fc1268bafa53cc2837/bce_python_sdk-0.9.35.tar.gz", hash = "sha256:024a2b5cd086707c866225cf8631fa126edbccfdd5bc3c8a83fe2ea9aa768bf5", size = 247844 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/91/c218750fd515fef10d197a2385a81a5f3504d30637fc1268bafa53cc2837/bce_python_sdk-0.9.35.tar.gz", hash = "sha256:024a2b5cd086707c866225cf8631fa126edbccfdd5bc3c8a83fe2ea9aa768bf5", size = 247844, upload-time = "2025-05-19T11:23:35.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/81/f574f6b300927a63596fa8e5081f5c0ad66d5cc99004d70d63c523f42ff8/bce_python_sdk-0.9.35-py3-none-any.whl", hash = "sha256:08c1575a0f2ec04b2fc17063fe6e47e1aab48e3bca1f26181cb8bed5528fa5de", size = 344813 }, + { url = "https://files.pythonhosted.org/packages/28/81/f574f6b300927a63596fa8e5081f5c0ad66d5cc99004d70d63c523f42ff8/bce_python_sdk-0.9.35-py3-none-any.whl", hash = "sha256:08c1575a0f2ec04b2fc17063fe6e47e1aab48e3bca1f26181cb8bed5528fa5de", size = 344813, upload-time = "2025-05-19T11:23:33.68Z" }, ] [[package]] name = "bcrypt" version = "4.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 }, - { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 }, - { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 }, - { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 }, - { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 }, - { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 }, - { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 }, - { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 }, - { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 }, - { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 }, - { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 }, - { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 }, - { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 }, - { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 }, - { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 }, - { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 }, - { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 }, - { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 }, - { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 }, - { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 }, - { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 }, - { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 }, - { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 }, - { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 }, - { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 }, - { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 }, - { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 }, - { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, - { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103 }, - { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513 }, - { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685 }, - { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110 }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, ] [[package]] @@ -505,27 +521,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/0b/44c39cf3b18a9280950ad63a579ce395dda4c32193ee9da7ff0aed547094/beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", size = 505113 } +sdist = { url = "https://files.pythonhosted.org/packages/af/0b/44c39cf3b18a9280950ad63a579ce395dda4c32193ee9da7ff0aed547094/beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", size = 505113, upload-time = "2023-04-07T15:02:49.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/f4/a69c20ee4f660081a7dedb1ac57f29be9378e04edfcb90c526b923d4bebc/beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a", size = 142979 }, + { url = "https://files.pythonhosted.org/packages/57/f4/a69c20ee4f660081a7dedb1ac57f29be9378e04edfcb90c526b923d4bebc/beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a", size = 142979, upload-time = "2023-04-07T15:02:50.77Z" }, ] [[package]] name = "billiard" version = "4.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031, upload-time = "2024-09-21T13:40:22.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766 }, + { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766, upload-time = "2024-09-21T13:40:20.188Z" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] @@ -537,23 +553,23 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/99/3e8b48f15580672eda20f33439fc1622bd611f6238b6d05407320e1fb98c/boto3-1.35.99.tar.gz", hash = "sha256:e0abd794a7a591d90558e92e29a9f8837d25ece8e3c120e530526fe27eba5fca", size = 111028 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/99/3e8b48f15580672eda20f33439fc1622bd611f6238b6d05407320e1fb98c/boto3-1.35.99.tar.gz", hash = "sha256:e0abd794a7a591d90558e92e29a9f8837d25ece8e3c120e530526fe27eba5fca", size = 111028, upload-time = "2025-01-14T20:20:28.636Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/77/8bbca82f70b062181cf0ae53fd43f1ac6556f3078884bfef9da2269c06a3/boto3-1.35.99-py3-none-any.whl", hash = "sha256:83e560faaec38a956dfb3d62e05e1703ee50432b45b788c09e25107c5058bd71", size = 139178 }, + { url = "https://files.pythonhosted.org/packages/65/77/8bbca82f70b062181cf0ae53fd43f1ac6556f3078884bfef9da2269c06a3/boto3-1.35.99-py3-none-any.whl", hash = "sha256:83e560faaec38a956dfb3d62e05e1703ee50432b45b788c09e25107c5058bd71", size = 139178, upload-time = "2025-01-14T20:20:25.48Z" }, ] [[package]] name = "boto3-stubs" -version = "1.38.28" +version = "1.39.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/dc/c4b318bdf66ff4973d3cbf0f218e47bdf558b94d921c8ff27d401e8ca4f9/boto3_stubs-1.38.28.tar.gz", hash = "sha256:fae8e009ea4d810b77e49d88247183b5d8d153bf268ebde8b4abdf58a701802f", size = 99065 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/ea/85b9940d6eedc04d0c6febf24d27311b6ee54f85ccc37192eb4db0dff5d6/boto3_stubs-1.39.3.tar.gz", hash = "sha256:9aad443b1d690951fd9ccb6fa20ad387bd0b1054c704566ff65dd0043a63fc26", size = 99947, upload-time = "2025-07-03T19:28:15.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/4a/d70ef76080e0d37e45d07d65801a6680f5bc1d156333a4e5d0ca6103d45f/boto3_stubs-1.38.28-py3-none-any.whl", hash = "sha256:accd8c0943a8d960c5c2d4a7aa661fd43e842893e76adf1b5b163c02f4f62537", size = 68668 }, + { url = "https://files.pythonhosted.org/packages/be/b8/0c56297e5f290de17e838c7e4ff338f5b94351c6566aed70ee197a671dc5/boto3_stubs-1.39.3-py3-none-any.whl", hash = "sha256:4daddb19374efa6d1bef7aded9cede0075f380722a9e60ab129ebba14ae66b69", size = 69196, upload-time = "2025-07-03T19:28:09.4Z" }, ] [package.optional-dependencies] @@ -570,21 +586,21 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/9c/1df6deceee17c88f7170bad8325aa91452529d683486273928eecfd946d8/botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3", size = 13490969 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/9c/1df6deceee17c88f7170bad8325aa91452529d683486273928eecfd946d8/botocore-1.35.99.tar.gz", hash = "sha256:1eab44e969c39c5f3d9a3104a0836c24715579a455f12b3979a31d7cde51b3c3", size = 13490969, upload-time = "2025-01-14T20:20:11.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/dd/d87e2a145fad9e08d0ec6edcf9d71f838ccc7acdd919acc4c0d4a93515f8/botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445", size = 13293216 }, + { url = "https://files.pythonhosted.org/packages/fc/dd/d87e2a145fad9e08d0ec6edcf9d71f838ccc7acdd919acc4c0d4a93515f8/botocore-1.35.99-py3-none-any.whl", hash = "sha256:b22d27b6b617fc2d7342090d6129000af2efd20174215948c0d7ae2da0fab445", size = 13293216, upload-time = "2025-01-14T20:20:06.427Z" }, ] [[package]] name = "botocore-stubs" -version = "1.38.28" +version = "1.38.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-awscrt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/0e/a47d392ee3fbd444a628f1b7257affc0a13c45e8742de4eb31fa4e2758f2/botocore_stubs-1.38.28.tar.gz", hash = "sha256:b9549050b81051bdbb91966323d6a2b6c5c78ca8dcc328e16dfc44b765be39be", size = 42312 } +sdist = { url = "https://files.pythonhosted.org/packages/05/45/27cabc7c3022dcb12de5098cc646b374065f5e72fae13600ff1756f365ee/botocore_stubs-1.38.46.tar.gz", hash = "sha256:a04e69766ab8bae338911c1897492f88d05cd489cd75f06e6eb4f135f9da8c7b", size = 42299, upload-time = "2025-06-29T22:58:24.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/3d/26c1a62b379f0dd401798f9f8f9331ac40f3c3917c34b5eed3ed0b85e2b0/botocore_stubs-1.38.28-py3-none-any.whl", hash = "sha256:9151ced682edb44b28202d268f4dc5ef5534be8b592b87e07ce4c99650818bce", size = 65629 }, + { url = "https://files.pythonhosted.org/packages/cc/84/06490071e26bab22ac79a684e98445df118adcf80c58c33ba5af184030f2/botocore_stubs-1.38.46-py3-none-any.whl", hash = "sha256:cc21d9a7dd994bdd90872db4664d817c4719b51cda8004fd507a4bf65b085a75", size = 66083, upload-time = "2025-06-29T22:58:22.234Z" }, ] [[package]] @@ -594,64 +610,64 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/82/dd20e69b97b9072ed2d26cc95c0a573461986bf62f7fde7ac59143490918/bottleneck-1.5.0.tar.gz", hash = "sha256:c860242cf20e69d5aab2ec3c5d6c8c2a15f19e4b25b28b8fca2c2a12cefae9d8", size = 104177 } +sdist = { url = "https://files.pythonhosted.org/packages/80/82/dd20e69b97b9072ed2d26cc95c0a573461986bf62f7fde7ac59143490918/bottleneck-1.5.0.tar.gz", hash = "sha256:c860242cf20e69d5aab2ec3c5d6c8c2a15f19e4b25b28b8fca2c2a12cefae9d8", size = 104177, upload-time = "2025-05-13T21:11:21.158Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/5e/d66b2487c12fa3343013ac87a03bcefbeacf5f13ffa4ad56bb4bce319d09/bottleneck-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9be5dfdf1a662d1d4423d7b7e8dd9a1b7046dcc2ce67b6e94a31d1cc57a8558f", size = 99536 }, - { url = "https://files.pythonhosted.org/packages/28/24/e7030fe27c7a9eb9cc8c86a4d74a7422d2c3e3466aecdf658617bea40491/bottleneck-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16fead35c0b5d307815997eef67d03c2151f255ca889e0fc3d68703f41aa5302", size = 357134 }, - { url = "https://files.pythonhosted.org/packages/d0/ce/91b0514a7ac456d934ebd90f0cae2314302f33c16e9489c99a4f496b1cff/bottleneck-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:049162927cf802208cc8691fb99b108afe74656cdc96b9e2067cf56cb9d84056", size = 361243 }, - { url = "https://files.pythonhosted.org/packages/be/f7/1a41889a6c0863b9f6236c14182bfb5f37c964e791b90ba721450817fc24/bottleneck-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2f5e863a4fdaf9c85416789aeb333d1cdd3603037fd854ad58b0e2ac73be16cf", size = 361326 }, - { url = "https://files.pythonhosted.org/packages/d3/e8/d4772b5321cf62b53c792253e38db1f6beee4f2de81e65bce5a6fe78df8e/bottleneck-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8d123762f78717fc35ecf10cad45d08273fcb12ab40b3c847190b83fec236f03", size = 371849 }, - { url = "https://files.pythonhosted.org/packages/29/dc/f88f6d476d7a3d6bd92f6e66f814d0bf088be20f0c6f716caa2a2ca02e82/bottleneck-1.5.0-cp311-cp311-win32.whl", hash = "sha256:07c2c1aa39917b5c9be77e85791aa598e8b2c00f8597a198b93628bbfde72a3f", size = 107710 }, - { url = "https://files.pythonhosted.org/packages/17/03/f89a2eff4f919a7c98433df3be6fd9787c72966a36be289ec180f505b2d5/bottleneck-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:80ef9eea2a92fc5a1c04734aa1bcf317253241062c962eaa6e7f123b583d0109", size = 112055 }, - { url = "https://files.pythonhosted.org/packages/8e/64/127e174cec548ab98bc0fa868b4f5d3ae5276e25c856d31d235d83d885a8/bottleneck-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbb0f0d38feda63050aa253cf9435e81a0ecfac954b0df84896636be9eabd9b6", size = 99640 }, - { url = "https://files.pythonhosted.org/packages/59/89/6e0b6463a36fd4771a9227d22ea904f892b80d95154399dd3e89fb6001f8/bottleneck-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:613165ce39bf6bd80f5307da0f05842ba534b213a89526f1eba82ea0099592fc", size = 358009 }, - { url = "https://files.pythonhosted.org/packages/f7/d6/7d1795a4a9e6383d3710a94c44010c7f2a8ba58cb5f2d9e2834a1c179afe/bottleneck-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f218e4dae6511180dcc4f06d8300e0c81e7f3df382091f464c5a919d289fab8e", size = 362875 }, - { url = "https://files.pythonhosted.org/packages/2b/1b/bab35ef291b9379a97e2fb986ce75f32eda38a47fc4954177b43590ee85e/bottleneck-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3886799cceb271eb67d057f6ecb13fb4582bda17a3b13b4fa0334638c59637c6", size = 361194 }, - { url = "https://files.pythonhosted.org/packages/d5/f3/a416fed726b81d2093578bc2112077f011c9f57b31e7ff3a1a9b00cce3d3/bottleneck-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc8d553d4bf033d3e025cd32d4c034d2daf10709e31ced3909811d1c843e451c", size = 373253 }, - { url = "https://files.pythonhosted.org/packages/0a/40/c372f9e59b3ce340d170fbdc24c12df3d2b3c22c4809b149b7129044180b/bottleneck-1.5.0-cp312-cp312-win32.whl", hash = "sha256:0dca825048a3076f34c4a35409e3277b31ceeb3cbb117bbe2a13ff5c214bcabc", size = 107915 }, - { url = "https://files.pythonhosted.org/packages/28/5a/57571a3cd4e356bbd636bb2225fbe916f29adc2235ba3dc77cd4085c91c8/bottleneck-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f26005740e6ef6013eba8a48241606a963e862a601671eab064b7835cd12ef3d", size = 112148 }, + { url = "https://files.pythonhosted.org/packages/fd/5e/d66b2487c12fa3343013ac87a03bcefbeacf5f13ffa4ad56bb4bce319d09/bottleneck-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9be5dfdf1a662d1d4423d7b7e8dd9a1b7046dcc2ce67b6e94a31d1cc57a8558f", size = 99536, upload-time = "2025-05-13T21:10:34.324Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/e7030fe27c7a9eb9cc8c86a4d74a7422d2c3e3466aecdf658617bea40491/bottleneck-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16fead35c0b5d307815997eef67d03c2151f255ca889e0fc3d68703f41aa5302", size = 357134, upload-time = "2025-05-13T21:10:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ce/91b0514a7ac456d934ebd90f0cae2314302f33c16e9489c99a4f496b1cff/bottleneck-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:049162927cf802208cc8691fb99b108afe74656cdc96b9e2067cf56cb9d84056", size = 361243, upload-time = "2025-05-13T21:10:36.851Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/1a41889a6c0863b9f6236c14182bfb5f37c964e791b90ba721450817fc24/bottleneck-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2f5e863a4fdaf9c85416789aeb333d1cdd3603037fd854ad58b0e2ac73be16cf", size = 361326, upload-time = "2025-05-13T21:10:37.904Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e8/d4772b5321cf62b53c792253e38db1f6beee4f2de81e65bce5a6fe78df8e/bottleneck-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8d123762f78717fc35ecf10cad45d08273fcb12ab40b3c847190b83fec236f03", size = 371849, upload-time = "2025-05-13T21:10:40.544Z" }, + { url = "https://files.pythonhosted.org/packages/29/dc/f88f6d476d7a3d6bd92f6e66f814d0bf088be20f0c6f716caa2a2ca02e82/bottleneck-1.5.0-cp311-cp311-win32.whl", hash = "sha256:07c2c1aa39917b5c9be77e85791aa598e8b2c00f8597a198b93628bbfde72a3f", size = 107710, upload-time = "2025-05-13T21:10:41.648Z" }, + { url = "https://files.pythonhosted.org/packages/17/03/f89a2eff4f919a7c98433df3be6fd9787c72966a36be289ec180f505b2d5/bottleneck-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:80ef9eea2a92fc5a1c04734aa1bcf317253241062c962eaa6e7f123b583d0109", size = 112055, upload-time = "2025-05-13T21:10:42.549Z" }, + { url = "https://files.pythonhosted.org/packages/8e/64/127e174cec548ab98bc0fa868b4f5d3ae5276e25c856d31d235d83d885a8/bottleneck-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbb0f0d38feda63050aa253cf9435e81a0ecfac954b0df84896636be9eabd9b6", size = 99640, upload-time = "2025-05-13T21:10:43.574Z" }, + { url = "https://files.pythonhosted.org/packages/59/89/6e0b6463a36fd4771a9227d22ea904f892b80d95154399dd3e89fb6001f8/bottleneck-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:613165ce39bf6bd80f5307da0f05842ba534b213a89526f1eba82ea0099592fc", size = 358009, upload-time = "2025-05-13T21:10:45.045Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d6/7d1795a4a9e6383d3710a94c44010c7f2a8ba58cb5f2d9e2834a1c179afe/bottleneck-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f218e4dae6511180dcc4f06d8300e0c81e7f3df382091f464c5a919d289fab8e", size = 362875, upload-time = "2025-05-13T21:10:46.16Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1b/bab35ef291b9379a97e2fb986ce75f32eda38a47fc4954177b43590ee85e/bottleneck-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3886799cceb271eb67d057f6ecb13fb4582bda17a3b13b4fa0334638c59637c6", size = 361194, upload-time = "2025-05-13T21:10:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f3/a416fed726b81d2093578bc2112077f011c9f57b31e7ff3a1a9b00cce3d3/bottleneck-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc8d553d4bf033d3e025cd32d4c034d2daf10709e31ced3909811d1c843e451c", size = 373253, upload-time = "2025-05-13T21:10:48.634Z" }, + { url = "https://files.pythonhosted.org/packages/0a/40/c372f9e59b3ce340d170fbdc24c12df3d2b3c22c4809b149b7129044180b/bottleneck-1.5.0-cp312-cp312-win32.whl", hash = "sha256:0dca825048a3076f34c4a35409e3277b31ceeb3cbb117bbe2a13ff5c214bcabc", size = 107915, upload-time = "2025-05-13T21:10:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/28/5a/57571a3cd4e356bbd636bb2225fbe916f29adc2235ba3dc77cd4085c91c8/bottleneck-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f26005740e6ef6013eba8a48241606a963e862a601671eab064b7835cd12ef3d", size = 112148, upload-time = "2025-05-13T21:10:51.626Z" }, ] [[package]] name = "brotli" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068 }, - { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950 }, - { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527 }, - { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080 }, - { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051 }, - { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172 }, - { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023 }, - { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871 }, - { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784 }, - { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905 }, - { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467 }, - { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169 }, - { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253 }, - { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693 }, - { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489 }, - { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081 }, - { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244 }, - { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505 }, - { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152 }, - { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252 }, - { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955 }, - { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304 }, - { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452 }, - { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751 }, - { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757 }, - { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146 }, - { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055 }, - { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102 }, - { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029 }, - { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276 }, - { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255 }, +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" }, + { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload-time = "2023-09-07T14:03:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload-time = "2023-09-07T14:03:44.552Z" }, + { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload-time = "2023-09-07T14:03:46.594Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload-time = "2023-09-07T14:03:48.204Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload-time = "2023-09-07T14:03:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload-time = "2023-09-07T14:03:52.395Z" }, + { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload-time = "2023-09-07T14:03:53.96Z" }, + { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload-time = "2024-10-18T12:32:16.688Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload-time = "2024-10-18T12:32:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload-time = "2024-10-18T12:32:20.192Z" }, + { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload-time = "2024-10-18T12:32:21.774Z" }, + { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169, upload-time = "2023-09-07T14:03:55.404Z" }, + { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253, upload-time = "2023-09-07T14:03:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" }, + { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" }, + { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" }, + { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" }, + { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276, upload-time = "2023-09-07T14:04:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload-time = "2023-09-07T14:04:17.83Z" }, ] [[package]] @@ -661,14 +677,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192 } +sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786 }, - { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165 }, - { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895 }, - { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834 }, - { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731 }, - { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783 }, + { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786, upload-time = "2023-09-14T14:21:57.72Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165, upload-time = "2023-09-14T14:21:59.613Z" }, + { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895, upload-time = "2023-09-14T14:22:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834, upload-time = "2023-09-14T14:22:03.571Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731, upload-time = "2023-09-14T14:22:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783, upload-time = "2023-09-14T14:22:07.096Z" }, ] [[package]] @@ -678,9 +694,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189 }, + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, ] [[package]] @@ -692,18 +708,18 @@ dependencies = [ { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950 }, + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, ] [[package]] name = "cachetools" version = "5.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/4d/27a3e6dd09011649ad5210bdf963765bc8fa81a0827a4fc01bafd2705c5b/cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105", size = 26522 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/4d/27a3e6dd09011649ad5210bdf963765bc8fa81a0827a4fc01bafd2705c5b/cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105", size = 26522, upload-time = "2024-02-26T20:33:23.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/2b/a64c2d25a37aeb921fddb929111413049fc5f8b9a4c1aefaffaafe768d54/cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", size = 9325 }, + { url = "https://files.pythonhosted.org/packages/fb/2b/a64c2d25a37aeb921fddb929111413049fc5f8b9a4c1aefaffaafe768d54/cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", size = 9325, upload-time = "2024-02-26T20:33:20.308Z" }, ] [[package]] @@ -720,18 +736,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775 }, + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, ] [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] @@ -741,75 +757,75 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, ] [[package]] name = "chardet" version = "5.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/32/cdc91dcf83849c7385bf8e2a5693d87376536ed000807fa07f5eab33430d/chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5", size = 2069617 } +sdist = { url = "https://files.pythonhosted.org/packages/41/32/cdc91dcf83849c7385bf8e2a5693d87376536ed000807fa07f5eab33430d/chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5", size = 2069617, upload-time = "2022-12-01T22:34:18.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/8f/8fc49109009e8d2169d94d72e6b1f4cd45c13d147ba7d6170fb41f22b08f/chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9", size = 199124 }, + { url = "https://files.pythonhosted.org/packages/74/8f/8fc49109009e8d2169d94d72e6b1f4cd45c13d147ba7d6170fb41f22b08f/chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9", size = 199124, upload-time = "2022-12-01T22:34:14.609Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] @@ -819,17 +835,17 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256 } +sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256, upload-time = "2024-07-22T20:19:29.259Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/af/d15fdfed2a204c0f9467ad35084fbac894c755820b203e62f5dcba2d41f1/chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca", size = 196911 }, - { url = "https://files.pythonhosted.org/packages/0d/19/aa6f2139f1ff7ad23a690ebf2a511b2594ab359915d7979f76f3213e46c4/chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f", size = 185000 }, - { url = "https://files.pythonhosted.org/packages/79/b1/1b269c750e985ec7d40b9bbe7d66d0a890e420525187786718e7f6b07913/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170", size = 2377289 }, - { url = "https://files.pythonhosted.org/packages/c7/2d/d5663e134436e5933bc63516a20b5edc08b4c1b1588b9680908a5f1afd04/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9", size = 2411755 }, - { url = "https://files.pythonhosted.org/packages/3e/79/1bce519cf186112d6d5ce2985392a89528c6e1e9332d680bf752694a4cdf/chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3", size = 151888 }, - { url = "https://files.pythonhosted.org/packages/93/ac/782b8d72de1c57b64fdf5cb94711540db99a92768d93d973174c62d45eb8/chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7", size = 197804 }, - { url = "https://files.pythonhosted.org/packages/32/4e/fd9ce0764228e9a98f6ff46af05e92804090b5557035968c5b4198bc7af9/chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912", size = 185421 }, - { url = "https://files.pythonhosted.org/packages/d9/3d/b59a8dedebd82545d873235ef2d06f95be244dfece7ee4a1a6044f080b18/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4", size = 2389672 }, - { url = "https://files.pythonhosted.org/packages/74/1e/80a033ea4466338824974a34f418e7b034a7748bf906f56466f5caa434b0/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5", size = 2436986 }, + { url = "https://files.pythonhosted.org/packages/f5/af/d15fdfed2a204c0f9467ad35084fbac894c755820b203e62f5dcba2d41f1/chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca", size = 196911, upload-time = "2024-07-22T20:18:33.46Z" }, + { url = "https://files.pythonhosted.org/packages/0d/19/aa6f2139f1ff7ad23a690ebf2a511b2594ab359915d7979f76f3213e46c4/chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f", size = 185000, upload-time = "2024-07-22T20:18:36.16Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/1b269c750e985ec7d40b9bbe7d66d0a890e420525187786718e7f6b07913/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170", size = 2377289, upload-time = "2024-07-22T20:18:37.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2d/d5663e134436e5933bc63516a20b5edc08b4c1b1588b9680908a5f1afd04/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9", size = 2411755, upload-time = "2024-07-22T20:18:39.949Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/1bce519cf186112d6d5ce2985392a89528c6e1e9332d680bf752694a4cdf/chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3", size = 151888, upload-time = "2024-07-22T20:18:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/93/ac/782b8d72de1c57b64fdf5cb94711540db99a92768d93d973174c62d45eb8/chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7", size = 197804, upload-time = "2024-07-22T20:18:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/fd9ce0764228e9a98f6ff46af05e92804090b5557035968c5b4198bc7af9/chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912", size = 185421, upload-time = "2024-07-22T20:18:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3d/b59a8dedebd82545d873235ef2d06f95be244dfece7ee4a1a6044f080b18/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4", size = 2389672, upload-time = "2024-07-22T20:18:49.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/1e/80a033ea4466338824974a34f418e7b034a7748bf906f56466f5caa434b0/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5", size = 2436986, upload-time = "2024-07-22T20:18:51.872Z" }, ] [[package]] @@ -866,9 +882,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/31/6c8e05405bb02b4a1f71f9aa3eef242415565dabf6afc1bde7f64f726963/chromadb-0.5.20.tar.gz", hash = "sha256:19513a23b2d20059866216bfd80195d1d4a160ffba234b8899f5e80978160ca7", size = 33664540 } +sdist = { url = "https://files.pythonhosted.org/packages/03/31/6c8e05405bb02b4a1f71f9aa3eef242415565dabf6afc1bde7f64f726963/chromadb-0.5.20.tar.gz", hash = "sha256:19513a23b2d20059866216bfd80195d1d4a160ffba234b8899f5e80978160ca7", size = 33664540, upload-time = "2024-11-19T05:13:58.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/7a/10bf5dc92d13cc03230190fcc5016a0b138d99e5b36b8b89ee0fe1680e10/chromadb-0.5.20-py3-none-any.whl", hash = "sha256:9550ba1b6dce911e35cac2568b301badf4b42f457b99a432bdeec2b6b9dd3680", size = 617884 }, + { url = "https://files.pythonhosted.org/packages/5f/7a/10bf5dc92d13cc03230190fcc5016a0b138d99e5b36b8b89ee0fe1680e10/chromadb-0.5.20-py3-none-any.whl", hash = "sha256:9550ba1b6dce911e35cac2568b301badf4b42f457b99a432bdeec2b6b9dd3680", size = 617884, upload-time = "2024-11-19T05:13:56.29Z" }, ] [[package]] @@ -878,9 +894,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -890,9 +906,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 }, + { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" }, ] [[package]] @@ -902,21 +918,21 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089 } +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631 }, + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, ] [[package]] name = "click-plugins" -version = "1.1.1" +version = "1.1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497 }, + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, ] [[package]] @@ -927,9 +943,9 @@ dependencies = [ { name = "click" }, { name = "prompt-toolkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289 }, + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, ] [[package]] @@ -943,28 +959,28 @@ dependencies = [ { name = "urllib3" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/8e/bf6012f7b45dbb74e19ad5c881a7bbcd1e7dd2b990f12cc434294d917800/clickhouse-connect-0.7.19.tar.gz", hash = "sha256:ce8f21f035781c5ef6ff57dc162e8150779c009b59f14030ba61f8c9c10c06d0", size = 84918 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/6f/a78cad40dc0f1fee19094c40abd7d23ff04bb491732c3a65b3661d426c89/clickhouse_connect-0.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee47af8926a7ec3a970e0ebf29a82cbbe3b1b7eae43336a81b3a0ca18091de5f", size = 253530 }, - { url = "https://files.pythonhosted.org/packages/40/82/419d110149900ace5eb0787c668d11e1657ac0eabb65c1404f039746f4ed/clickhouse_connect-0.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce429233b2d21a8a149c8cd836a2555393cbcf23d61233520db332942ffb8964", size = 245691 }, - { url = "https://files.pythonhosted.org/packages/e3/9c/ad6708ced6cf9418334d2bf19bbba3c223511ed852eb85f79b1e7c20cdbd/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617c04f5c46eed3344a7861cd96fb05293e70d3b40d21541b1e459e7574efa96", size = 1055273 }, - { url = "https://files.pythonhosted.org/packages/ea/99/88c24542d6218100793cfb13af54d7ad4143d6515b0b3d621ba3b5a2d8af/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08e33b8cc2dc1873edc5ee4088d4fc3c0dbb69b00e057547bcdc7e9680b43e5", size = 1067030 }, - { url = "https://files.pythonhosted.org/packages/c8/84/19eb776b4e760317c21214c811f04f612cba7eee0f2818a7d6806898a994/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:921886b887f762e5cc3eef57ef784d419a3f66df85fd86fa2e7fbbf464c4c54a", size = 1027207 }, - { url = "https://files.pythonhosted.org/packages/22/81/c2982a33b088b6c9af5d0bdc46413adc5fedceae063b1f8b56570bb28887/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ad0cf8552a9e985cfa6524b674ae7c8f5ba51df5bd3ecddbd86c82cdbef41a7", size = 1054850 }, - { url = "https://files.pythonhosted.org/packages/7b/a4/4a84ed3e92323d12700011cc8c4039f00a8c888079d65e75a4d4758ba288/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70f838ef0861cdf0e2e198171a1f3fd2ee05cf58e93495eeb9b17dfafb278186", size = 1022784 }, - { url = "https://files.pythonhosted.org/packages/5e/67/3f5cc6f78c9adbbd6a3183a3f9f3196a116be19e958d7eaa6e307b391fed/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c5f0d207cb0dcc1adb28ced63f872d080924b7562b263a9d54d4693b670eb066", size = 1071084 }, - { url = "https://files.pythonhosted.org/packages/01/8d/a294e1cc752e22bc6ee08aa421ea31ed9559b09d46d35499449140a5c374/clickhouse_connect-0.7.19-cp311-cp311-win32.whl", hash = "sha256:8c96c4c242b98fcf8005e678a26dbd4361748721b6fa158c1fe84ad15c7edbbe", size = 221156 }, - { url = "https://files.pythonhosted.org/packages/68/69/09b3a4e53f5d3d770e9fa70f6f04642cdb37cc76d37279c55fd4e868f845/clickhouse_connect-0.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:bda092bab224875ed7c7683707d63f8a2322df654c4716e6611893a18d83e908", size = 238826 }, - { url = "https://files.pythonhosted.org/packages/af/f8/1d48719728bac33c1a9815e0a7230940e078fd985b09af2371715de78a3c/clickhouse_connect-0.7.19-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f170d08166438d29f0dcfc8a91b672c783dc751945559e65eefff55096f9274", size = 256687 }, - { url = "https://files.pythonhosted.org/packages/ed/0d/3cbbbd204be045c4727f9007679ad97d3d1d559b43ba844373a79af54d16/clickhouse_connect-0.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26b80cb8f66bde9149a9a2180e2cc4895c1b7d34f9dceba81630a9b9a9ae66b2", size = 247631 }, - { url = "https://files.pythonhosted.org/packages/b6/44/adb55285226d60e9c46331a9980c88dad8c8de12abb895c4e3149a088092/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba80e3598acf916c4d1b2515671f65d9efee612a783c17c56a5a646f4db59b9", size = 1053767 }, - { url = "https://files.pythonhosted.org/packages/6c/f3/a109c26a41153768be57374cb823cac5daf74c9098a5c61081ffabeb4e59/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d38c30bd847af0ce7ff738152478f913854db356af4d5824096394d0eab873d", size = 1072014 }, - { url = "https://files.pythonhosted.org/packages/51/80/9c200e5e392a538f2444c9a6a93e1cf0e36588c7e8720882ac001e23b246/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d41d4b159071c0e4f607563932d4fa5c2a8fc27d3ba1200d0929b361e5191864", size = 1027423 }, - { url = "https://files.pythonhosted.org/packages/33/a3/219fcd1572f1ce198dcef86da8c6c526b04f56e8b7a82e21119677f89379/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3682c2426f5dbda574611210e3c7c951b9557293a49eb60a7438552435873889", size = 1053683 }, - { url = "https://files.pythonhosted.org/packages/5d/df/687d90fbc0fd8ce586c46400f3791deac120e4c080aa8b343c0f676dfb08/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6d492064dca278eb61be3a2d70a5f082e2ebc8ceebd4f33752ae234116192020", size = 1021120 }, - { url = "https://files.pythonhosted.org/packages/c8/3b/39ba71b103275df8ec90d424dbaca2dba82b28398c3d2aeac5a0141b6aae/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:62612da163b934c1ff35df6155a47cf17ac0e2d2f9f0f8f913641e5c02cdf39f", size = 1073652 }, - { url = "https://files.pythonhosted.org/packages/b3/92/06df8790a7d93d5d5f1098604fc7d79682784818030091966a3ce3f766a8/clickhouse_connect-0.7.19-cp312-cp312-win32.whl", hash = "sha256:196e48c977affc045794ec7281b4d711e169def00535ecab5f9fdeb8c177f149", size = 221589 }, - { url = "https://files.pythonhosted.org/packages/42/1f/935d0810b73184a1d306f92458cb0a2e9b0de2377f536da874e063b8e422/clickhouse_connect-0.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:b771ca6a473d65103dcae82810d3a62475c5372fc38d8f211513c72b954fb020", size = 239584 }, +sdist = { url = "https://files.pythonhosted.org/packages/f4/8e/bf6012f7b45dbb74e19ad5c881a7bbcd1e7dd2b990f12cc434294d917800/clickhouse-connect-0.7.19.tar.gz", hash = "sha256:ce8f21f035781c5ef6ff57dc162e8150779c009b59f14030ba61f8c9c10c06d0", size = 84918, upload-time = "2024-08-21T21:37:16.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/6f/a78cad40dc0f1fee19094c40abd7d23ff04bb491732c3a65b3661d426c89/clickhouse_connect-0.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee47af8926a7ec3a970e0ebf29a82cbbe3b1b7eae43336a81b3a0ca18091de5f", size = 253530, upload-time = "2024-08-21T21:35:53.372Z" }, + { url = "https://files.pythonhosted.org/packages/40/82/419d110149900ace5eb0787c668d11e1657ac0eabb65c1404f039746f4ed/clickhouse_connect-0.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce429233b2d21a8a149c8cd836a2555393cbcf23d61233520db332942ffb8964", size = 245691, upload-time = "2024-08-21T21:35:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/ad6708ced6cf9418334d2bf19bbba3c223511ed852eb85f79b1e7c20cdbd/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617c04f5c46eed3344a7861cd96fb05293e70d3b40d21541b1e459e7574efa96", size = 1055273, upload-time = "2024-08-21T21:35:56.478Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/88c24542d6218100793cfb13af54d7ad4143d6515b0b3d621ba3b5a2d8af/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08e33b8cc2dc1873edc5ee4088d4fc3c0dbb69b00e057547bcdc7e9680b43e5", size = 1067030, upload-time = "2024-08-21T21:35:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/c8/84/19eb776b4e760317c21214c811f04f612cba7eee0f2818a7d6806898a994/clickhouse_connect-0.7.19-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:921886b887f762e5cc3eef57ef784d419a3f66df85fd86fa2e7fbbf464c4c54a", size = 1027207, upload-time = "2024-08-21T21:35:59.832Z" }, + { url = "https://files.pythonhosted.org/packages/22/81/c2982a33b088b6c9af5d0bdc46413adc5fedceae063b1f8b56570bb28887/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ad0cf8552a9e985cfa6524b674ae7c8f5ba51df5bd3ecddbd86c82cdbef41a7", size = 1054850, upload-time = "2024-08-21T21:36:01.559Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a4/4a84ed3e92323d12700011cc8c4039f00a8c888079d65e75a4d4758ba288/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70f838ef0861cdf0e2e198171a1f3fd2ee05cf58e93495eeb9b17dfafb278186", size = 1022784, upload-time = "2024-08-21T21:36:02.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/3f5cc6f78c9adbbd6a3183a3f9f3196a116be19e958d7eaa6e307b391fed/clickhouse_connect-0.7.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c5f0d207cb0dcc1adb28ced63f872d080924b7562b263a9d54d4693b670eb066", size = 1071084, upload-time = "2024-08-21T21:36:04.052Z" }, + { url = "https://files.pythonhosted.org/packages/01/8d/a294e1cc752e22bc6ee08aa421ea31ed9559b09d46d35499449140a5c374/clickhouse_connect-0.7.19-cp311-cp311-win32.whl", hash = "sha256:8c96c4c242b98fcf8005e678a26dbd4361748721b6fa158c1fe84ad15c7edbbe", size = 221156, upload-time = "2024-08-21T21:36:05.72Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/09b3a4e53f5d3d770e9fa70f6f04642cdb37cc76d37279c55fd4e868f845/clickhouse_connect-0.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:bda092bab224875ed7c7683707d63f8a2322df654c4716e6611893a18d83e908", size = 238826, upload-time = "2024-08-21T21:36:06.892Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/1d48719728bac33c1a9815e0a7230940e078fd985b09af2371715de78a3c/clickhouse_connect-0.7.19-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f170d08166438d29f0dcfc8a91b672c783dc751945559e65eefff55096f9274", size = 256687, upload-time = "2024-08-21T21:36:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0d/3cbbbd204be045c4727f9007679ad97d3d1d559b43ba844373a79af54d16/clickhouse_connect-0.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26b80cb8f66bde9149a9a2180e2cc4895c1b7d34f9dceba81630a9b9a9ae66b2", size = 247631, upload-time = "2024-08-21T21:36:09.679Z" }, + { url = "https://files.pythonhosted.org/packages/b6/44/adb55285226d60e9c46331a9980c88dad8c8de12abb895c4e3149a088092/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba80e3598acf916c4d1b2515671f65d9efee612a783c17c56a5a646f4db59b9", size = 1053767, upload-time = "2024-08-21T21:36:11.361Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f3/a109c26a41153768be57374cb823cac5daf74c9098a5c61081ffabeb4e59/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d38c30bd847af0ce7ff738152478f913854db356af4d5824096394d0eab873d", size = 1072014, upload-time = "2024-08-21T21:36:12.752Z" }, + { url = "https://files.pythonhosted.org/packages/51/80/9c200e5e392a538f2444c9a6a93e1cf0e36588c7e8720882ac001e23b246/clickhouse_connect-0.7.19-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d41d4b159071c0e4f607563932d4fa5c2a8fc27d3ba1200d0929b361e5191864", size = 1027423, upload-time = "2024-08-21T21:36:14.483Z" }, + { url = "https://files.pythonhosted.org/packages/33/a3/219fcd1572f1ce198dcef86da8c6c526b04f56e8b7a82e21119677f89379/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3682c2426f5dbda574611210e3c7c951b9557293a49eb60a7438552435873889", size = 1053683, upload-time = "2024-08-21T21:36:15.828Z" }, + { url = "https://files.pythonhosted.org/packages/5d/df/687d90fbc0fd8ce586c46400f3791deac120e4c080aa8b343c0f676dfb08/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6d492064dca278eb61be3a2d70a5f082e2ebc8ceebd4f33752ae234116192020", size = 1021120, upload-time = "2024-08-21T21:36:17.184Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3b/39ba71b103275df8ec90d424dbaca2dba82b28398c3d2aeac5a0141b6aae/clickhouse_connect-0.7.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:62612da163b934c1ff35df6155a47cf17ac0e2d2f9f0f8f913641e5c02cdf39f", size = 1073652, upload-time = "2024-08-21T21:36:19.053Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/06df8790a7d93d5d5f1098604fc7d79682784818030091966a3ce3f766a8/clickhouse_connect-0.7.19-cp312-cp312-win32.whl", hash = "sha256:196e48c977affc045794ec7281b4d711e169def00535ecab5f9fdeb8c177f149", size = 221589, upload-time = "2024-08-21T21:36:20.796Z" }, + { url = "https://files.pythonhosted.org/packages/42/1f/935d0810b73184a1d306f92458cb0a2e9b0de2377f536da874e063b8e422/clickhouse_connect-0.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:b771ca6a473d65103dcae82810d3a62475c5372fc38d8f211513c72b954fb020", size = 239584, upload-time = "2024-08-21T21:36:22.105Z" }, ] [[package]] @@ -976,18 +992,18 @@ dependencies = [ { name = "requests" }, { name = "requests-toolbelt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/25/6d0481860583f44953bd791de0b7c4f6d7ead7223f8a17e776247b34a5b4/cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", size = 93261 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/25/6d0481860583f44953bd791de0b7c4f6d7ead7223f8a17e776247b34a5b4/cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", size = 93261, upload-time = "2023-04-25T23:20:19.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/97/fc88803a451029688dffd7eb446dc1b529657577aec13aceff1cc9628c5d/cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0", size = 99652 }, + { url = "https://files.pythonhosted.org/packages/81/97/fc88803a451029688dffd7eb446dc1b529657577aec13aceff1cc9628c5d/cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0", size = 99652, upload-time = "2023-04-25T23:20:15.974Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -997,9 +1013,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "humanfriendly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] [[package]] @@ -1013,53 +1029,53 @@ dependencies = [ { name = "six" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/f2/be99b41433b33a76896680920fca621f191875ca410a66778015e47a501b/cos-python-sdk-v5-1.9.30.tar.gz", hash = "sha256:a23fd090211bf90883066d90cd74317860aa67c6d3aa80fe5e44b18c7e9b2a81", size = 108384 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f2/be99b41433b33a76896680920fca621f191875ca410a66778015e47a501b/cos-python-sdk-v5-1.9.30.tar.gz", hash = "sha256:a23fd090211bf90883066d90cd74317860aa67c6d3aa80fe5e44b18c7e9b2a81", size = 108384, upload-time = "2024-06-14T08:02:37.063Z" } [[package]] name = "couchbase" version = "4.3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/70/7cf92b2443330e7a4b626a02fe15fbeb1531337d75e6ae6393294e960d18/couchbase-4.3.6.tar.gz", hash = "sha256:d58c5ccdad5d85fc026f328bf4190c4fc0041fdbe68ad900fb32fc5497c3f061", size = 6517695 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/70/7cf92b2443330e7a4b626a02fe15fbeb1531337d75e6ae6393294e960d18/couchbase-4.3.6.tar.gz", hash = "sha256:d58c5ccdad5d85fc026f328bf4190c4fc0041fdbe68ad900fb32fc5497c3f061", size = 6517695, upload-time = "2025-05-15T17:21:38.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/eae21d3a9331f7c93e8483f686e1bcb9e3b48f2ce98193beb0637a620926/couchbase-4.3.6-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:4c10fd26271c5630196b9bcc0dd7e17a45fa9c7e46ed5756e5690d125423160c", size = 4775710 }, - { url = "https://files.pythonhosted.org/packages/f6/98/0ca042a42f5807bbf8050f52fff39ebceebc7bea7e5897907758f3e1ad39/couchbase-4.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:811eee7a6013cea7b15a718e201ee1188df162c656d27c7882b618ab57a08f3a", size = 4020743 }, - { url = "https://files.pythonhosted.org/packages/f8/0f/c91407cb082d2322217e8f7ca4abb8eda016a81a4db5a74b7ac6b737597d/couchbase-4.3.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fc177e0161beb1e6e8c4b9561efcb97c51aed55a77ee11836ca194d33ae22b7", size = 4796091 }, - { url = "https://files.pythonhosted.org/packages/8c/02/5567b660543828bdbbc68dcae080e388cb0be391aa8a97cce9d8c8a6c147/couchbase-4.3.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02afb1c1edd6b215f702510412b5177ed609df8135930c23789bbc5901dd1b45", size = 5015684 }, - { url = "https://files.pythonhosted.org/packages/dc/d1/767908826d5bdd258addab26d7f1d21bc42bafbf5f30d1b556ace06295af/couchbase-4.3.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:594e9eb17bb76ba8e10eeee17a16aef897dd90d33c6771cf2b5b4091da415b32", size = 5673513 }, - { url = "https://files.pythonhosted.org/packages/f2/25/39ecde0a06692abce8bb0df4f15542933f05883647a1a57cdc7bbed9c77c/couchbase-4.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:db22c56e38b8313f65807aa48309c8b8c7c44d5517b9ff1d8b4404d4740ec286", size = 4010728 }, - { url = "https://files.pythonhosted.org/packages/b1/55/c12b8f626de71363fbe30578f4a0de1b8bb41afbe7646ff8538c3b38ce2a/couchbase-4.3.6-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:a2ae13432b859f513485d4cee691e1e4fce4af23ed4218b9355874b146343f8c", size = 4693517 }, - { url = "https://files.pythonhosted.org/packages/a1/aa/2184934d283d99b34a004f577bf724d918278a2962781ca5690d4fa4b6c6/couchbase-4.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ea5ca7e34b5d023c8bab406211ab5d71e74a976ba25fa693b4f8e6c74f85aa2", size = 4022393 }, - { url = "https://files.pythonhosted.org/packages/80/29/ba6d3b205a51c04c270c1b56ea31da678b7edc565b35a34237ec2cfc708d/couchbase-4.3.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6eaca0a71fd8f9af4344b7d6474d7b74d1784ae9a658f6bc3751df5f9a4185ae", size = 4798396 }, - { url = "https://files.pythonhosted.org/packages/4a/94/d7d791808bd9064c01f965015ff40ee76e6bac10eaf2c73308023b9bdedf/couchbase-4.3.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0470378b986f69368caed6d668ac6530e635b0c1abaef3d3f524cfac0dacd878", size = 5018099 }, - { url = "https://files.pythonhosted.org/packages/a6/04/cec160f9f4b862788e2a0167616472a5695b2f569bd62204938ab674835d/couchbase-4.3.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:374ce392558f1688ac073aa0b15c256b1a441201d965811fd862357ff05d27a9", size = 5672633 }, - { url = "https://files.pythonhosted.org/packages/1b/a2/1da2ab45412b9414e2c6a578e0e7a24f29b9261ef7de11707c2fc98045b8/couchbase-4.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:cd734333de34d8594504c163bb6c47aea9cc1f2cefdf8e91875dd9bf14e61e29", size = 4013298 }, + { url = "https://files.pythonhosted.org/packages/f3/0a/eae21d3a9331f7c93e8483f686e1bcb9e3b48f2ce98193beb0637a620926/couchbase-4.3.6-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:4c10fd26271c5630196b9bcc0dd7e17a45fa9c7e46ed5756e5690d125423160c", size = 4775710, upload-time = "2025-05-15T17:20:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/0ca042a42f5807bbf8050f52fff39ebceebc7bea7e5897907758f3e1ad39/couchbase-4.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:811eee7a6013cea7b15a718e201ee1188df162c656d27c7882b618ab57a08f3a", size = 4020743, upload-time = "2025-05-15T17:20:31.515Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0f/c91407cb082d2322217e8f7ca4abb8eda016a81a4db5a74b7ac6b737597d/couchbase-4.3.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fc177e0161beb1e6e8c4b9561efcb97c51aed55a77ee11836ca194d33ae22b7", size = 4796091, upload-time = "2025-05-15T17:20:33.818Z" }, + { url = "https://files.pythonhosted.org/packages/8c/02/5567b660543828bdbbc68dcae080e388cb0be391aa8a97cce9d8c8a6c147/couchbase-4.3.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02afb1c1edd6b215f702510412b5177ed609df8135930c23789bbc5901dd1b45", size = 5015684, upload-time = "2025-05-15T17:20:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/767908826d5bdd258addab26d7f1d21bc42bafbf5f30d1b556ace06295af/couchbase-4.3.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:594e9eb17bb76ba8e10eeee17a16aef897dd90d33c6771cf2b5b4091da415b32", size = 5673513, upload-time = "2025-05-15T17:20:38.972Z" }, + { url = "https://files.pythonhosted.org/packages/f2/25/39ecde0a06692abce8bb0df4f15542933f05883647a1a57cdc7bbed9c77c/couchbase-4.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:db22c56e38b8313f65807aa48309c8b8c7c44d5517b9ff1d8b4404d4740ec286", size = 4010728, upload-time = "2025-05-15T17:20:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/b1/55/c12b8f626de71363fbe30578f4a0de1b8bb41afbe7646ff8538c3b38ce2a/couchbase-4.3.6-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:a2ae13432b859f513485d4cee691e1e4fce4af23ed4218b9355874b146343f8c", size = 4693517, upload-time = "2025-05-15T17:20:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/a1/aa/2184934d283d99b34a004f577bf724d918278a2962781ca5690d4fa4b6c6/couchbase-4.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ea5ca7e34b5d023c8bab406211ab5d71e74a976ba25fa693b4f8e6c74f85aa2", size = 4022393, upload-time = "2025-05-15T17:20:47.442Z" }, + { url = "https://files.pythonhosted.org/packages/80/29/ba6d3b205a51c04c270c1b56ea31da678b7edc565b35a34237ec2cfc708d/couchbase-4.3.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6eaca0a71fd8f9af4344b7d6474d7b74d1784ae9a658f6bc3751df5f9a4185ae", size = 4798396, upload-time = "2025-05-15T17:20:49.473Z" }, + { url = "https://files.pythonhosted.org/packages/4a/94/d7d791808bd9064c01f965015ff40ee76e6bac10eaf2c73308023b9bdedf/couchbase-4.3.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0470378b986f69368caed6d668ac6530e635b0c1abaef3d3f524cfac0dacd878", size = 5018099, upload-time = "2025-05-15T17:20:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/a6/04/cec160f9f4b862788e2a0167616472a5695b2f569bd62204938ab674835d/couchbase-4.3.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:374ce392558f1688ac073aa0b15c256b1a441201d965811fd862357ff05d27a9", size = 5672633, upload-time = "2025-05-15T17:20:55.994Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/1da2ab45412b9414e2c6a578e0e7a24f29b9261ef7de11707c2fc98045b8/couchbase-4.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:cd734333de34d8594504c163bb6c47aea9cc1f2cefdf8e91875dd9bf14e61e29", size = 4013298, upload-time = "2025-05-15T17:20:59.533Z" }, ] [[package]] name = "coverage" version = "7.2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895 }, - { url = "https://files.pythonhosted.org/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120 }, - { url = "https://files.pythonhosted.org/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178 }, - { url = "https://files.pythonhosted.org/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754 }, - { url = "https://files.pythonhosted.org/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558 }, - { url = "https://files.pythonhosted.org/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509 }, - { url = "https://files.pythonhosted.org/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924 }, - { url = "https://files.pythonhosted.org/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977 }, - { url = "https://files.pythonhosted.org/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168 }, - { url = "https://files.pythonhosted.org/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185 }, - { url = "https://files.pythonhosted.org/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020 }, - { url = "https://files.pythonhosted.org/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994 }, - { url = "https://files.pythonhosted.org/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358 }, - { url = "https://files.pythonhosted.org/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316 }, - { url = "https://files.pythonhosted.org/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159 }, - { url = "https://files.pythonhosted.org/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127 }, - { url = "https://files.pythonhosted.org/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833 }, - { url = "https://files.pythonhosted.org/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463 }, - { url = "https://files.pythonhosted.org/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347 }, +sdist = { url = "https://files.pythonhosted.org/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575, upload-time = "2023-05-29T20:08:50.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895, upload-time = "2023-05-29T20:07:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120, upload-time = "2023-05-29T20:07:23.765Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178, upload-time = "2023-05-29T20:07:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754, upload-time = "2023-05-29T20:07:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558, upload-time = "2023-05-29T20:07:28.743Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509, upload-time = "2023-05-29T20:07:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924, upload-time = "2023-05-29T20:07:32.065Z" }, + { url = "https://files.pythonhosted.org/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977, upload-time = "2023-05-29T20:07:34.184Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168, upload-time = "2023-05-29T20:07:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185, upload-time = "2023-05-29T20:07:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020, upload-time = "2023-05-29T20:07:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994, upload-time = "2023-05-29T20:07:40.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358, upload-time = "2023-05-29T20:07:41.998Z" }, + { url = "https://files.pythonhosted.org/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316, upload-time = "2023-05-29T20:07:43.539Z" }, + { url = "https://files.pythonhosted.org/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159, upload-time = "2023-05-29T20:07:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127, upload-time = "2023-05-29T20:07:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833, upload-time = "2023-05-29T20:07:47.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463, upload-time = "2023-05-29T20:07:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347, upload-time = "2023-05-29T20:07:51.909Z" }, ] [package.optional-dependencies] @@ -1071,70 +1087,77 @@ toml = [ name = "crc32c" version = "2.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/4c/4e40cc26347ac8254d3f25b9f94710b8e8df24ee4dddc1ba41907a88a94d/crc32c-2.7.1.tar.gz", hash = "sha256:f91b144a21eef834d64178e01982bb9179c354b3e9e5f4c803b0e5096384968c", size = 45712 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/8e/2f37f46368bbfd50edfc11b96f0aa135699034b1b020966c70ebaff3463b/crc32c-2.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19e03a50545a3ef400bd41667d5525f71030488629c57d819e2dd45064f16192", size = 49672 }, - { url = "https://files.pythonhosted.org/packages/ed/b8/e52f7c4b045b871c2984d70f37c31d4861b533a8082912dfd107a96cf7c1/crc32c-2.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c03286b1e5ce9bed7090084f206aacd87c5146b4b10de56fe9e86cbbbf851cf", size = 37155 }, - { url = "https://files.pythonhosted.org/packages/25/ee/0cfa82a68736697f3c7e435ba658c2ef8c997f42b89f6ab4545efe1b2649/crc32c-2.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ebbf144a1a56a532b353e81fa0f3edca4f4baa1bf92b1dde2c663a32bb6a15", size = 35372 }, - { url = "https://files.pythonhosted.org/packages/aa/92/c878aaba81c431fcd93a059e9f6c90db397c585742793f0bf6e0c531cc67/crc32c-2.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96b794fd11945298fdd5eb1290a812efb497c14bc42592c5c992ca077458eeba", size = 54879 }, - { url = "https://files.pythonhosted.org/packages/5b/f5/ab828ab3907095e06b18918408748950a9f726ee2b37be1b0839fb925ee1/crc32c-2.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df7194dd3c0efb5a21f5d70595b7a8b4fd9921fbbd597d6d8e7a11eca3e2d27", size = 52588 }, - { url = "https://files.pythonhosted.org/packages/6a/2b/9e29e9ac4c4213d60491db09487125db358cd9263490fbadbd55e48fbe03/crc32c-2.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d698eec444b18e296a104d0b9bb6c596c38bdcb79d24eba49604636e9d747305", size = 53674 }, - { url = "https://files.pythonhosted.org/packages/79/ed/df3c4c14bf1b29f5c9b52d51fb6793e39efcffd80b2941d994e8f7f5f688/crc32c-2.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e07cf10ef852d219d179333fd706d1c415626f1f05e60bd75acf0143a4d8b225", size = 54691 }, - { url = "https://files.pythonhosted.org/packages/0c/47/4917af3c9c1df2fff28bbfa6492673c9adeae5599dcc207bbe209847489c/crc32c-2.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2a051f296e6e92e13efee3b41db388931cdb4a2800656cd1ed1d9fe4f13a086", size = 52896 }, - { url = "https://files.pythonhosted.org/packages/1b/6f/26fc3dda5835cda8f6cd9d856afe62bdeae428de4c34fea200b0888e8835/crc32c-2.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1738259802978cdf428f74156175da6a5fdfb7256f647fdc0c9de1bc6cd7173", size = 53554 }, - { url = "https://files.pythonhosted.org/packages/56/3e/6f39127f7027c75d130c0ba348d86a6150dff23761fbc6a5f71659f4521e/crc32c-2.7.1-cp311-cp311-win32.whl", hash = "sha256:f7786d219a1a1bf27d0aa1869821d11a6f8e90415cfffc1e37791690d4a848a1", size = 38370 }, - { url = "https://files.pythonhosted.org/packages/c9/fb/1587c2705a3a47a3d0067eecf9a6fec510761c96dec45c7b038fb5c8ff46/crc32c-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:887f6844bb3ad35f0778cd10793ad217f7123a5422e40041231b8c4c7329649d", size = 39795 }, - { url = "https://files.pythonhosted.org/packages/1d/02/998dc21333413ce63fe4c1ca70eafe61ca26afc7eb353f20cecdb77d614e/crc32c-2.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7d1c4e761fe42bf856130daf8b2658df33fe0ced3c43dadafdfeaa42b57b950", size = 49568 }, - { url = "https://files.pythonhosted.org/packages/9c/3e/e3656bfa76e50ef87b7136fef2dbf3c46e225629432fc9184fdd7fd187ff/crc32c-2.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:73361c79a6e4605204457f19fda18b042a94508a52e53d10a4239da5fb0f6a34", size = 37019 }, - { url = "https://files.pythonhosted.org/packages/0b/7d/5ff9904046ad15a08772515db19df43107bf5e3901a89c36a577b5f40ba0/crc32c-2.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:afd778fc8ac0ed2ffbfb122a9aa6a0e409a8019b894a1799cda12c01534493e0", size = 35373 }, - { url = "https://files.pythonhosted.org/packages/4d/41/4aedc961893f26858ab89fc772d0eaba91f9870f19eaa933999dcacb94ec/crc32c-2.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ef661b34e9f25991fface7f9ad85e81bbc1b3fe3b916fd58c893eabe2fa0b8", size = 54675 }, - { url = "https://files.pythonhosted.org/packages/d6/63/8cabf09b7e39b9fec8f7010646c8b33057fc8d67e6093b3cc15563d23533/crc32c-2.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:571aa4429444b5d7f588e4377663592145d2d25eb1635abb530f1281794fc7c9", size = 52386 }, - { url = "https://files.pythonhosted.org/packages/79/13/13576941bf7cf95026abae43d8427c812c0054408212bf8ed490eda846b0/crc32c-2.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c02a3bd67dea95cdb25844aaf44ca2e1b0c1fd70b287ad08c874a95ef4bb38db", size = 53495 }, - { url = "https://files.pythonhosted.org/packages/3d/b6/55ffb26d0517d2d6c6f430ce2ad36ae7647c995c5bfd7abce7f32bb2bad1/crc32c-2.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d17637c4867672cb8adeea007294e3c3df9d43964369516cfe2c1f47ce500a", size = 54456 }, - { url = "https://files.pythonhosted.org/packages/c2/1a/5562e54cb629ecc5543d3604dba86ddfc7c7b7bf31d64005b38a00d31d31/crc32c-2.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4a400ac3c69a32e180d8753fd7ec7bccb80ade7ab0812855dce8a208e72495f", size = 52647 }, - { url = "https://files.pythonhosted.org/packages/48/ec/ce4138eaf356cd9aae60bbe931755e5e0151b3eca5f491fce6c01b97fd59/crc32c-2.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:588587772e55624dd9c7a906ec9e8773ae0b6ac5e270fc0bc84ee2758eba90d5", size = 53332 }, - { url = "https://files.pythonhosted.org/packages/5e/b5/144b42cd838a901175a916078781cb2c3c9f977151c9ba085aebd6d15b22/crc32c-2.7.1-cp312-cp312-win32.whl", hash = "sha256:9f14b60e5a14206e8173dd617fa0c4df35e098a305594082f930dae5488da428", size = 38371 }, - { url = "https://files.pythonhosted.org/packages/ae/c4/7929dcd5d9b57db0cce4fe6f6c191049380fc6d8c9b9f5581967f4ec018e/crc32c-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:7c810a246660a24dc818047dc5f89c7ce7b2814e1e08a8e99993f4103f7219e8", size = 39805 }, +sdist = { url = "https://files.pythonhosted.org/packages/7f/4c/4e40cc26347ac8254d3f25b9f94710b8e8df24ee4dddc1ba41907a88a94d/crc32c-2.7.1.tar.gz", hash = "sha256:f91b144a21eef834d64178e01982bb9179c354b3e9e5f4c803b0e5096384968c", size = 45712, upload-time = "2024-09-24T06:20:17.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/8e/2f37f46368bbfd50edfc11b96f0aa135699034b1b020966c70ebaff3463b/crc32c-2.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19e03a50545a3ef400bd41667d5525f71030488629c57d819e2dd45064f16192", size = 49672, upload-time = "2024-09-24T06:18:18.032Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b8/e52f7c4b045b871c2984d70f37c31d4861b533a8082912dfd107a96cf7c1/crc32c-2.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c03286b1e5ce9bed7090084f206aacd87c5146b4b10de56fe9e86cbbbf851cf", size = 37155, upload-time = "2024-09-24T06:18:19.373Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/0cfa82a68736697f3c7e435ba658c2ef8c997f42b89f6ab4545efe1b2649/crc32c-2.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ebbf144a1a56a532b353e81fa0f3edca4f4baa1bf92b1dde2c663a32bb6a15", size = 35372, upload-time = "2024-09-24T06:18:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/aa/92/c878aaba81c431fcd93a059e9f6c90db397c585742793f0bf6e0c531cc67/crc32c-2.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96b794fd11945298fdd5eb1290a812efb497c14bc42592c5c992ca077458eeba", size = 54879, upload-time = "2024-09-24T06:18:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f5/ab828ab3907095e06b18918408748950a9f726ee2b37be1b0839fb925ee1/crc32c-2.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df7194dd3c0efb5a21f5d70595b7a8b4fd9921fbbd597d6d8e7a11eca3e2d27", size = 52588, upload-time = "2024-09-24T06:18:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2b/9e29e9ac4c4213d60491db09487125db358cd9263490fbadbd55e48fbe03/crc32c-2.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d698eec444b18e296a104d0b9bb6c596c38bdcb79d24eba49604636e9d747305", size = 53674, upload-time = "2024-09-24T06:18:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/79/ed/df3c4c14bf1b29f5c9b52d51fb6793e39efcffd80b2941d994e8f7f5f688/crc32c-2.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e07cf10ef852d219d179333fd706d1c415626f1f05e60bd75acf0143a4d8b225", size = 54691, upload-time = "2024-09-24T06:18:26.578Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/4917af3c9c1df2fff28bbfa6492673c9adeae5599dcc207bbe209847489c/crc32c-2.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2a051f296e6e92e13efee3b41db388931cdb4a2800656cd1ed1d9fe4f13a086", size = 52896, upload-time = "2024-09-24T06:18:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6f/26fc3dda5835cda8f6cd9d856afe62bdeae428de4c34fea200b0888e8835/crc32c-2.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1738259802978cdf428f74156175da6a5fdfb7256f647fdc0c9de1bc6cd7173", size = 53554, upload-time = "2024-09-24T06:18:29.104Z" }, + { url = "https://files.pythonhosted.org/packages/56/3e/6f39127f7027c75d130c0ba348d86a6150dff23761fbc6a5f71659f4521e/crc32c-2.7.1-cp311-cp311-win32.whl", hash = "sha256:f7786d219a1a1bf27d0aa1869821d11a6f8e90415cfffc1e37791690d4a848a1", size = 38370, upload-time = "2024-09-24T06:18:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fb/1587c2705a3a47a3d0067eecf9a6fec510761c96dec45c7b038fb5c8ff46/crc32c-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:887f6844bb3ad35f0778cd10793ad217f7123a5422e40041231b8c4c7329649d", size = 39795, upload-time = "2024-09-24T06:18:31.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/02/998dc21333413ce63fe4c1ca70eafe61ca26afc7eb353f20cecdb77d614e/crc32c-2.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7d1c4e761fe42bf856130daf8b2658df33fe0ced3c43dadafdfeaa42b57b950", size = 49568, upload-time = "2024-09-24T06:18:32.425Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3e/e3656bfa76e50ef87b7136fef2dbf3c46e225629432fc9184fdd7fd187ff/crc32c-2.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:73361c79a6e4605204457f19fda18b042a94508a52e53d10a4239da5fb0f6a34", size = 37019, upload-time = "2024-09-24T06:18:34.097Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7d/5ff9904046ad15a08772515db19df43107bf5e3901a89c36a577b5f40ba0/crc32c-2.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:afd778fc8ac0ed2ffbfb122a9aa6a0e409a8019b894a1799cda12c01534493e0", size = 35373, upload-time = "2024-09-24T06:18:35.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/41/4aedc961893f26858ab89fc772d0eaba91f9870f19eaa933999dcacb94ec/crc32c-2.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56ef661b34e9f25991fface7f9ad85e81bbc1b3fe3b916fd58c893eabe2fa0b8", size = 54675, upload-time = "2024-09-24T06:18:35.954Z" }, + { url = "https://files.pythonhosted.org/packages/d6/63/8cabf09b7e39b9fec8f7010646c8b33057fc8d67e6093b3cc15563d23533/crc32c-2.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:571aa4429444b5d7f588e4377663592145d2d25eb1635abb530f1281794fc7c9", size = 52386, upload-time = "2024-09-24T06:18:36.896Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/13576941bf7cf95026abae43d8427c812c0054408212bf8ed490eda846b0/crc32c-2.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c02a3bd67dea95cdb25844aaf44ca2e1b0c1fd70b287ad08c874a95ef4bb38db", size = 53495, upload-time = "2024-09-24T06:18:38.099Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/55ffb26d0517d2d6c6f430ce2ad36ae7647c995c5bfd7abce7f32bb2bad1/crc32c-2.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d17637c4867672cb8adeea007294e3c3df9d43964369516cfe2c1f47ce500a", size = 54456, upload-time = "2024-09-24T06:18:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/5562e54cb629ecc5543d3604dba86ddfc7c7b7bf31d64005b38a00d31d31/crc32c-2.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4a400ac3c69a32e180d8753fd7ec7bccb80ade7ab0812855dce8a208e72495f", size = 52647, upload-time = "2024-09-24T06:18:40.021Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/ce4138eaf356cd9aae60bbe931755e5e0151b3eca5f491fce6c01b97fd59/crc32c-2.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:588587772e55624dd9c7a906ec9e8773ae0b6ac5e270fc0bc84ee2758eba90d5", size = 53332, upload-time = "2024-09-24T06:18:40.925Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/144b42cd838a901175a916078781cb2c3c9f977151c9ba085aebd6d15b22/crc32c-2.7.1-cp312-cp312-win32.whl", hash = "sha256:9f14b60e5a14206e8173dd617fa0c4df35e098a305594082f930dae5488da428", size = 38371, upload-time = "2024-09-24T06:18:42.711Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c4/7929dcd5d9b57db0cce4fe6f6c191049380fc6d8c9b9f5581967f4ec018e/crc32c-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:7c810a246660a24dc818047dc5f89c7ce7b2814e1e08a8e99993f4103f7219e8", size = 39805, upload-time = "2024-09-24T06:18:43.6Z" }, ] [[package]] name = "crcmod" version = "1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } [[package]] name = "cryptography" -version = "42.0.8" +version = "45.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/a7/1498799a2ea06148463a9a2c10ab2f6a921a74fb19e231b27dc412a748e2/cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", size = 671250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/8b/1b929ba8139430e09e140e6939c2b29c18df1f2fc2149e41bdbdcdaf5d1f/cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", size = 5899961 }, - { url = "https://files.pythonhosted.org/packages/fa/5d/31d833daa800e4fab33209843095df7adb4a78ea536929145534cbc15026/cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", size = 3114353 }, - { url = "https://files.pythonhosted.org/packages/5d/32/f6326c70a9f0f258a201d3b2632bca586ea24d214cec3cf36e374040e273/cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", size = 3647773 }, - { url = "https://files.pythonhosted.org/packages/35/66/2d87e9ca95c82c7ee5f2c09716fc4c4242c1ae6647b9bd27e55e920e9f10/cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", size = 3839763 }, - { url = "https://files.pythonhosted.org/packages/c2/de/8083fa2e68d403553a01a9323f4f8b9d7ffed09928ba25635c29fb28c1e7/cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", size = 3632661 }, - { url = "https://files.pythonhosted.org/packages/07/40/d6f6819c62e808ea74639c3c640f7edd636b86cce62cb14943996a15df92/cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", size = 3851536 }, - { url = "https://files.pythonhosted.org/packages/5c/46/de71d48abf2b6d3c808f4fbb0f4dc44a4e72786be23df0541aa2a3f6fd7e/cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", size = 3754209 }, - { url = "https://files.pythonhosted.org/packages/25/c9/86f04e150c5d5d5e4a731a2c1e0e43da84d901f388e3fea3d5de98d689a7/cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", size = 3923551 }, - { url = "https://files.pythonhosted.org/packages/53/c2/903014dafb7271fb148887d4355b2e90319cad6e810663be622b0c933fc9/cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", size = 3739265 }, - { url = "https://files.pythonhosted.org/packages/95/26/82d704d988a193cbdc69ac3b41c687c36eaed1642cce52530ad810c35645/cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", size = 3937371 }, - { url = "https://files.pythonhosted.org/packages/cf/71/4e0d05c9acd638a225f57fb6162aa3d03613c11b76893c23ea4675bb28c5/cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", size = 2438849 }, - { url = "https://files.pythonhosted.org/packages/06/0f/78da3cad74f2ba6c45321dc90394d70420ea846730dc042ef527f5a224b5/cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", size = 2889090 }, - { url = "https://files.pythonhosted.org/packages/60/12/f064af29190cdb1d38fe07f3db6126091639e1dece7ec77c4ff037d49193/cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", size = 5901232 }, - { url = "https://files.pythonhosted.org/packages/43/c2/4a3eef67e009a522711ebd8ac89424c3a7fe591ece7035d964419ad52a1d/cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", size = 3648711 }, - { url = "https://files.pythonhosted.org/packages/49/1c/9f6d13cc8041c05eebff1154e4e71bedd1db8e174fff999054435994187a/cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", size = 3841968 }, - { url = "https://files.pythonhosted.org/packages/5f/f9/c3d4f19b82bdb25a3d857fe96e7e571c981810e47e3f299cc13ac429066a/cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", size = 3633032 }, - { url = "https://files.pythonhosted.org/packages/fa/e2/b7e6e8c261536c489d9cf908769880d94bd5d9a187e166b0dc838d2e6a56/cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", size = 3852478 }, - { url = "https://files.pythonhosted.org/packages/a2/68/e16751f6b859bc120f53fddbf3ebada5c34f0e9689d8af32884d8b2e4b4c/cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e", size = 3754102 }, - { url = "https://files.pythonhosted.org/packages/0f/38/85c74d0ac4c540780e072b1e6f148ecb718418c1062edcb20d22f3ec5bbb/cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", size = 3925042 }, - { url = "https://files.pythonhosted.org/packages/89/f4/a8b982e88eb5350407ebdbf4717b55043271d878705329e107f4783555f2/cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", size = 3738833 }, - { url = "https://files.pythonhosted.org/packages/fd/2b/be327b580645927bb1a1f32d5a175b897a9b956bc085b095e15c40bac9ed/cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", size = 3938751 }, - { url = "https://files.pythonhosted.org/packages/3c/d5/c6a78ffccdbe4516711ebaa9ed2c7eb6ac5dfa3dc920f2c7e920af2418b0/cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", size = 2439281 }, - { url = "https://files.pythonhosted.org/packages/a2/7b/b0d330852dd5953daee6b15f742f15d9f18e9c0154eb4cfcc8718f0436da/cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", size = 2886038 }, +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, + { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" }, ] [[package]] @@ -1145,27 +1168,27 @@ dependencies = [ { name = "marshmallow" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 } +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 }, + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] @@ -1175,9 +1198,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, ] [[package]] @@ -1187,15 +1210,17 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] [[package]] name = "dify-api" +version = "1.6.0" source = { virtual = "." } dependencies = [ + { name = "arize-phoenix-otel" }, { name = "authlib" }, { name = "azure-identity" }, { name = "beautifulsoup4" }, @@ -1221,6 +1246,7 @@ dependencies = [ { name = "googleapis-common-protos" }, { name = "gunicorn" }, { name = "httpx", extra = ["socks"] }, + { name = "httpx-sse" }, { name = "jieba" }, { name = "json-repair" }, { name = "langfuse" }, @@ -1247,7 +1273,6 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "opik" }, { name = "pandas", extra = ["excel", "output-formatting", "performance"] }, - { name = "pandas-stubs" }, { name = "pandoc" }, { name = "psycogreen" }, { name = "psycopg2-binary" }, @@ -1263,8 +1288,10 @@ dependencies = [ { name = "readabilipy" }, { name = "redis", extra = ["hiredis"] }, { name = "resend" }, + { name = "sendgrid" }, { name = "sentry-sdk", extra = ["flask"] }, { name = "sqlalchemy" }, + { name = "sseclient-py" }, { name = "starlette" }, { name = "tiktoken" }, { name = "transformers" }, @@ -1280,14 +1307,17 @@ dev = [ { name = "coverage" }, { name = "dotenv-linter" }, { name = "faker" }, + { name = "hypothesis" }, { name = "lxml-stubs" }, { name = "mypy" }, + { name = "pandas-stubs" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, { name = "pytest-env" }, { name = "pytest-mock" }, { name = "ruff" }, + { name = "scipy-stubs" }, { name = "types-aiofiles" }, { name = "types-beautifulsoup4" }, { name = "types-cachetools" }, @@ -1316,6 +1346,7 @@ dev = [ { name = "types-pymysql" }, { name = "types-pyopenssl" }, { name = "types-python-dateutil" }, + { name = "types-python-http-client" }, { name = "types-pywin32" }, { name = "types-pyyaml" }, { name = "types-regex" }, @@ -1351,6 +1382,7 @@ vdb = [ { name = "clickhouse-connect" }, { name = "couchbase" }, { name = "elasticsearch" }, + { name = "mo-vector" }, { name = "opensearch-py" }, { name = "oracledb" }, { name = "pgvecto-rs", extra = ["sqlalchemy"] }, @@ -1370,6 +1402,7 @@ vdb = [ [package.metadata] requires-dist = [ + { name = "arize-phoenix-otel", specifier = "~=0.9.2" }, { name = "authlib", specifier = "==1.3.1" }, { name = "azure-identity", specifier = "==1.16.1" }, { name = "beautifulsoup4", specifier = "==4.12.2" }, @@ -1395,6 +1428,7 @@ requires-dist = [ { name = "googleapis-common-protos", specifier = "==1.63.0" }, { name = "gunicorn", specifier = "~=23.0.0" }, { name = "httpx", extras = ["socks"], specifier = "~=0.27.0" }, + { name = "httpx-sse", specifier = ">=0.4.0" }, { name = "jieba", specifier = "==0.42.1" }, { name = "json-repair", specifier = ">=0.41.1" }, { name = "langfuse", specifier = "~=2.51.3" }, @@ -1421,7 +1455,6 @@ requires-dist = [ { name = "opentelemetry-util-http", specifier = "==0.48b0" }, { name = "opik", specifier = "~=1.7.25" }, { name = "pandas", extras = ["excel", "output-formatting", "performance"], specifier = "~=2.2.2" }, - { name = "pandas-stubs", specifier = "~=2.2.3.241009" }, { name = "pandoc", specifier = "~=2.4" }, { name = "psycogreen", specifier = "~=1.0.2" }, { name = "psycopg2-binary", specifier = "~=2.9.6" }, @@ -1437,8 +1470,10 @@ requires-dist = [ { name = "readabilipy", specifier = "~=0.3.0" }, { name = "redis", extras = ["hiredis"], specifier = "~=6.1.0" }, { name = "resend", specifier = "~=2.9.0" }, + { name = "sendgrid", specifier = "~=6.12.3" }, { name = "sentry-sdk", extras = ["flask"], specifier = "~=2.28.0" }, { name = "sqlalchemy", specifier = "~=2.0.29" }, + { name = "sseclient-py", specifier = ">=1.8.0" }, { name = "starlette", specifier = "==0.41.0" }, { name = "tiktoken", specifier = "~=0.9.0" }, { name = "transformers", specifier = "~=4.51.0" }, @@ -1454,14 +1489,17 @@ dev = [ { name = "coverage", specifier = "~=7.2.4" }, { name = "dotenv-linter", specifier = "~=0.5.0" }, { name = "faker", specifier = "~=32.1.0" }, + { name = "hypothesis", specifier = ">=6.131.15" }, { name = "lxml-stubs", specifier = "~=0.5.1" }, - { name = "mypy", specifier = "~=1.15.0" }, + { name = "mypy", specifier = "~=1.16.0" }, + { name = "pandas-stubs", specifier = "~=2.2.3" }, { name = "pytest", specifier = "~=8.3.2" }, { name = "pytest-benchmark", specifier = "~=4.0.0" }, { name = "pytest-cov", specifier = "~=4.1.0" }, { name = "pytest-env", specifier = "~=1.1.3" }, { name = "pytest-mock", specifier = "~=3.14.0" }, - { name = "ruff", specifier = "~=0.11.5" }, + { name = "ruff", specifier = "~=0.12.3" }, + { name = "scipy-stubs", specifier = ">=1.15.3.0" }, { name = "types-aiofiles", specifier = "~=24.1.0" }, { name = "types-beautifulsoup4", specifier = "~=4.12.0" }, { name = "types-cachetools", specifier = "~=5.5.0" }, @@ -1490,6 +1528,7 @@ dev = [ { name = "types-pymysql", specifier = "~=1.1.0" }, { name = "types-pyopenssl", specifier = ">=24.1.0" }, { name = "types-python-dateutil", specifier = "~=2.9.0" }, + { name = "types-python-http-client", specifier = ">=3.3.7.20240910" }, { name = "types-pywin32", specifier = "~=310.0.0" }, { name = "types-pyyaml", specifier = "~=6.0.12" }, { name = "types-regex", specifier = "~=2024.11.6" }, @@ -1525,6 +1564,7 @@ vdb = [ { name = "clickhouse-connect", specifier = "~=0.7.16" }, { name = "couchbase", specifier = "~=4.3.0" }, { name = "elasticsearch", specifier = "==8.14.0" }, + { name = "mo-vector", specifier = "~=0.1.13" }, { name = "opensearch-py", specifier = "==2.4.0" }, { name = "oracledb", specifier = "==3.0.0" }, { name = "pgvecto-rs", extras = ["sqlalchemy"], specifier = "~=0.2.1" }, @@ -1533,7 +1573,7 @@ vdb = [ { name = "pymochow", specifier = "==1.3.1" }, { name = "pyobvector", specifier = "~=0.1.6" }, { name = "qdrant-client", specifier = "==1.9.0" }, - { name = "tablestore", specifier = "==6.1.0" }, + { name = "tablestore", specifier = "==6.2.0" }, { name = "tcvectordb", specifier = "~=1.6.4" }, { name = "tidb-vector", specifier = "==0.0.9" }, { name = "upstash-vector", specifier = "==0.6.0" }, @@ -1546,39 +1586,27 @@ vdb = [ name = "diskcache" version = "5.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, -] - -[[package]] -name = "docker-pycreds" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/e6/d1f6c00b7221e2d7c4b470132c931325c8b22c51ca62417e300f5ce16009/docker-pycreds-0.4.0.tar.gz", hash = "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4", size = 8754 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/e8/f6bd1eee09314e7e6dee49cbe2c5e22314ccdb38db16c9fc72d2fa80d054/docker_pycreds-0.4.0-py2.py3-none-any.whl", hash = "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49", size = 8982 }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[package]] name = "docstring-parser" version = "0.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, ] [[package]] @@ -1592,18 +1620,30 @@ dependencies = [ { name = "ply" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/fe/77e184ccc312f6263cbcc48a9579eec99f5c7ff72a9b1bd7812cafc22bbb/dotenv_linter-0.5.0.tar.gz", hash = "sha256:4862a8393e5ecdfb32982f1b32dbc006fff969a7b3c8608ba7db536108beeaea", size = 15346 } +sdist = { url = "https://files.pythonhosted.org/packages/ef/fe/77e184ccc312f6263cbcc48a9579eec99f5c7ff72a9b1bd7812cafc22bbb/dotenv_linter-0.5.0.tar.gz", hash = "sha256:4862a8393e5ecdfb32982f1b32dbc006fff969a7b3c8608ba7db536108beeaea", size = 15346, upload-time = "2024-03-13T11:52:10.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/01/62ed4374340e6cf17c5084828974d96db8085e4018439ac41dc3cbbbcab3/dotenv_linter-0.5.0-py3-none-any.whl", hash = "sha256:fd01cca7f2140cb1710f49cbc1bf0e62397a75a6f0522d26a8b9b2331143c8bd", size = 21770 }, + { url = "https://files.pythonhosted.org/packages/f0/01/62ed4374340e6cf17c5084828974d96db8085e4018439ac41dc3cbbbcab3/dotenv_linter-0.5.0-py3-none-any.whl", hash = "sha256:fd01cca7f2140cb1710f49cbc1bf0e62397a75a6f0522d26a8b9b2331143c8bd", size = 21770, upload-time = "2024-03-13T11:52:08.607Z" }, ] [[package]] name = "durationpy" version = "0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 }, + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, ] [[package]] @@ -1614,9 +1654,9 @@ dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/54/d498a766ac8fa475f931da85a154666cc81a70f8eb4a780bc8e4e934e9ac/elastic_transport-8.17.1.tar.gz", hash = "sha256:5edef32ac864dca8e2f0a613ef63491ee8d6b8cfb52881fa7313ba9290cac6d2", size = 73425 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/54/d498a766ac8fa475f931da85a154666cc81a70f8eb4a780bc8e4e934e9ac/elastic_transport-8.17.1.tar.gz", hash = "sha256:5edef32ac864dca8e2f0a613ef63491ee8d6b8cfb52881fa7313ba9290cac6d2", size = 73425, upload-time = "2025-03-13T07:28:30.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/cd/b71d5bc74cde7fc6fd9b2ff9389890f45d9762cbbbf81dc5e51fd7588c4a/elastic_transport-8.17.1-py3-none-any.whl", hash = "sha256:192718f498f1d10c5e9aa8b9cf32aed405e469a7f0e9d6a8923431dbb2c59fb8", size = 64969 }, + { url = "https://files.pythonhosted.org/packages/cf/cd/b71d5bc74cde7fc6fd9b2ff9389890f45d9762cbbbf81dc5e51fd7588c4a/elastic_transport-8.17.1-py3-none-any.whl", hash = "sha256:192718f498f1d10c5e9aa8b9cf32aed405e469a7f0e9d6a8923431dbb2c59fb8", size = 64969, upload-time = "2025-03-13T07:28:29.031Z" }, ] [[package]] @@ -1626,27 +1666,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "elastic-transport" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/63/8dc82cbf1bfbca2a2af8eeaa4a7eccc2cf7a87bf217130f6bc66d33b4d8f/elasticsearch-8.14.0.tar.gz", hash = "sha256:aa2490029dd96f4015b333c1827aa21fd6c0a4d223b00dfb0fe933b8d09a511b", size = 382506 } +sdist = { url = "https://files.pythonhosted.org/packages/36/63/8dc82cbf1bfbca2a2af8eeaa4a7eccc2cf7a87bf217130f6bc66d33b4d8f/elasticsearch-8.14.0.tar.gz", hash = "sha256:aa2490029dd96f4015b333c1827aa21fd6c0a4d223b00dfb0fe933b8d09a511b", size = 382506, upload-time = "2024-06-06T13:31:10.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/c9dec8bd95bff6aaa8fe29a834257a6606608d0b2ed9932a1857683f736f/elasticsearch-8.14.0-py3-none-any.whl", hash = "sha256:cef8ef70a81af027f3da74a4f7d9296b390c636903088439087b8262a468c130", size = 480236 }, + { url = "https://files.pythonhosted.org/packages/a2/09/c9dec8bd95bff6aaa8fe29a834257a6606608d0b2ed9932a1857683f736f/elasticsearch-8.14.0-py3-none-any.whl", hash = "sha256:cef8ef70a81af027f3da74a4f7d9296b390c636903088439087b8262a468c130", size = 480236, upload-time = "2024-06-06T13:31:00.987Z" }, ] [[package]] name = "emoji" version = "2.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617 }, -] - -[[package]] -name = "enum34" -version = "1.1.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/c4/2da1f4952ba476677a42f25cd32ab8aaf0e1c0d0e00b89822b835c7e654c/enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248", size = 28187 } +sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182, upload-time = "2025-01-16T06:31:24.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f6/ccb1c83687756aeabbf3ca0f213508fcfb03883ff200d201b3a4c60cedcc/enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328", size = 11224 }, + { url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617, upload-time = "2025-01-16T06:31:23.526Z" }, ] [[package]] @@ -1656,15 +1687,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycryptodome" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/af/d83276f9e288bd6a62f44d67ae1eafd401028ba1b2b643ae4014b51da5bd/esdk-obs-python-3.24.6.1.tar.gz", hash = "sha256:c45fed143e99d9256c8560c1d78f651eae0d2e809d16e962f8b286b773c33bf0", size = 85798 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/af/d83276f9e288bd6a62f44d67ae1eafd401028ba1b2b643ae4014b51da5bd/esdk-obs-python-3.24.6.1.tar.gz", hash = "sha256:c45fed143e99d9256c8560c1d78f651eae0d2e809d16e962f8b286b773c33bf0", size = 85798, upload-time = "2024-07-26T13:13:22.467Z" } [[package]] name = "et-xmlfile" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] [[package]] @@ -1675,41 +1706,41 @@ dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/2a/dd2c8f55d69013d0eee30ec4c998250fb7da957f5fe860ed077b3df1725b/faker-32.1.0.tar.gz", hash = "sha256:aac536ba04e6b7beb2332c67df78485fc29c1880ff723beac6d1efd45e2f10f5", size = 1850193 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/2a/dd2c8f55d69013d0eee30ec4c998250fb7da957f5fe860ed077b3df1725b/faker-32.1.0.tar.gz", hash = "sha256:aac536ba04e6b7beb2332c67df78485fc29c1880ff723beac6d1efd45e2f10f5", size = 1850193, upload-time = "2024-11-12T22:04:34.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/fa/4a82dea32d6262a96e6841cdd4a45c11ac09eecdff018e745565410ac70e/Faker-32.1.0-py3-none-any.whl", hash = "sha256:c77522577863c264bdc9dad3a2a750ad3f7ee43ff8185072e482992288898814", size = 1889123 }, + { url = "https://files.pythonhosted.org/packages/7e/fa/4a82dea32d6262a96e6841cdd4a45c11ac09eecdff018e745565410ac70e/Faker-32.1.0-py3-none-any.whl", hash = "sha256:c77522577863c264bdc9dad3a2a750ad3f7ee43ff8185072e482992288898814", size = 1889123, upload-time = "2024-11-12T22:04:32.298Z" }, ] [[package]] name = "fastapi" -version = "0.115.12" +version = "0.116.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +sdist = { url = "https://files.pythonhosted.org/packages/20/38/e1da78736143fd885c36213a3ccc493c384ae8fea6a0f0bc272ef42ebea8/fastapi-0.116.0.tar.gz", hash = "sha256:80dc0794627af0390353a6d1171618276616310d37d24faba6648398e57d687a", size = 296518, upload-time = "2025-07-07T15:09:27.82Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { url = "https://files.pythonhosted.org/packages/2f/68/d80347fe2360445b5f58cf290e588a4729746e7501080947e6cdae114b1f/fastapi-0.116.0-py3-none-any.whl", hash = "sha256:fdcc9ed272eaef038952923bef2b735c02372402d1203ee1210af4eea7a78d2b", size = 95625, upload-time = "2025-07-07T15:09:26.348Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "filetype" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] [[package]] @@ -1724,9 +1755,9 @@ dependencies = [ { name = "markupsafe" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305 }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" }, ] [[package]] @@ -1740,22 +1771,22 @@ dependencies = [ { name = "zstandard" }, { name = "zstandard", extra = ["cffi"], marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/1f/260db5a4517d59bfde7b4a0d71052df68fb84983bda9231100e3b80f5989/flask_compress-1.17.tar.gz", hash = "sha256:1ebb112b129ea7c9e7d6ee6d5cc0d64f226cbc50c4daddf1a58b9bd02253fbd8", size = 15733 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/1f/260db5a4517d59bfde7b4a0d71052df68fb84983bda9231100e3b80f5989/flask_compress-1.17.tar.gz", hash = "sha256:1ebb112b129ea7c9e7d6ee6d5cc0d64f226cbc50c4daddf1a58b9bd02253fbd8", size = 15733, upload-time = "2024-10-14T08:13:33.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/54/ff08f947d07c0a8a5d8f1c8e57b142c97748ca912b259db6467ab35983cd/Flask_Compress-1.17-py3-none-any.whl", hash = "sha256:415131f197c41109f08e8fdfc3a6628d83d81680fb5ecd0b3a97410e02397b20", size = 8723 }, + { url = "https://files.pythonhosted.org/packages/f7/54/ff08f947d07c0a8a5d8f1c8e57b142c97748ca912b259db6467ab35983cd/Flask_Compress-1.17-py3-none-any.whl", hash = "sha256:415131f197c41109f08e8fdfc3a6628d83d81680fb5ecd0b3a97410e02397b20", size = 8723, upload-time = "2024-10-14T08:13:31.726Z" }, ] [[package]] name = "flask-cors" -version = "6.0.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/e7/b3c6afdd984672b55dff07482699c688af6c01bd7fd5dd55f9c9d1a88d1c/flask_cors-6.0.0.tar.gz", hash = "sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393", size = 11875 } +sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/f0/0ee29090016345938f016ee98aa8b5de1c500ee93491dc0c76495848fca1/flask_cors-6.0.0-py3-none-any.whl", hash = "sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657", size = 11549 }, + { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" }, ] [[package]] @@ -1766,9 +1797,9 @@ dependencies = [ { name = "flask" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303 }, + { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" }, ] [[package]] @@ -1780,9 +1811,9 @@ dependencies = [ { name = "flask" }, { name = "flask-sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/e2/4008fc0d298d7ce797021b194bbe151d4d12db670691648a226d4fc8aefc/Flask-Migrate-4.0.7.tar.gz", hash = "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622", size = 21770 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/e2/4008fc0d298d7ce797021b194bbe151d4d12db670691648a226d4fc8aefc/Flask-Migrate-4.0.7.tar.gz", hash = "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622", size = 21770, upload-time = "2024-03-11T18:43:01.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/01/587023575286236f95d2ab8a826c320375ed5ea2102bb103ed89704ffa6b/Flask_Migrate-4.0.7-py3-none-any.whl", hash = "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", size = 21127 }, + { url = "https://files.pythonhosted.org/packages/93/01/587023575286236f95d2ab8a826c320375ed5ea2102bb103ed89704ffa6b/Flask_Migrate-4.0.7-py3-none-any.whl", hash = "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", size = 21127, upload-time = "2024-03-11T18:42:59.462Z" }, ] [[package]] @@ -1795,9 +1826,9 @@ dependencies = [ { name = "pytz" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/ce/a0a133db616ea47f78a41e15c4c68b9f08cab3df31eb960f61899200a119/Flask-RESTful-0.3.10.tar.gz", hash = "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37", size = 110453 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/ce/a0a133db616ea47f78a41e15c4c68b9f08cab3df31eb960f61899200a119/Flask-RESTful-0.3.10.tar.gz", hash = "sha256:fe4af2ef0027df8f9b4f797aba20c5566801b6ade995ac63b588abf1a59cec37", size = 110453, upload-time = "2023-05-21T03:58:55.781Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/7b/f0b45f0df7d2978e5ae51804bb5939b7897b2ace24306009da0cc34d8d1f/Flask_RESTful-0.3.10-py2.py3-none-any.whl", hash = "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b", size = 26217 }, + { url = "https://files.pythonhosted.org/packages/d7/7b/f0b45f0df7d2978e5ae51804bb5939b7897b2ace24306009da0cc34d8d1f/Flask_RESTful-0.3.10-py2.py3-none-any.whl", hash = "sha256:1cf93c535172f112e080b0d4503a8d15f93a48c88bdd36dd87269bdaf405051b", size = 26217, upload-time = "2023-05-21T03:58:54.004Z" }, ] [[package]] @@ -1808,79 +1839,79 @@ dependencies = [ { name = "flask" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899 } +sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125 }, + { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" }, ] [[package]] name = "flatbuffers" version = "25.2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170, upload-time = "2025-02-11T04:26:46.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 }, + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload-time = "2025-02-11T04:26:44.484Z" }, ] [[package]] name = "frozenlist" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/b5/bc883b5296ec902115c00be161da93bf661199c465ec4c483feec6ea4c32/frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", size = 160912 }, - { url = "https://files.pythonhosted.org/packages/6f/93/51b058b563d0704b39c56baa222828043aafcac17fd3734bec5dbeb619b1/frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", size = 124315 }, - { url = "https://files.pythonhosted.org/packages/c9/e0/46cd35219428d350558b874d595e132d1c17a9471a1bd0d01d518a261e7c/frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", size = 122230 }, - { url = "https://files.pythonhosted.org/packages/d1/0f/7ad2ce928ad06d6dd26a61812b959ded573d3e9d0ee6109d96c2be7172e9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", size = 314842 }, - { url = "https://files.pythonhosted.org/packages/34/76/98cbbd8a20a5c3359a2004ae5e5b216af84a150ccbad67c8f8f30fb2ea91/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", size = 304919 }, - { url = "https://files.pythonhosted.org/packages/9a/fa/258e771ce3a44348c05e6b01dffc2bc67603fba95761458c238cd09a2c77/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", size = 324074 }, - { url = "https://files.pythonhosted.org/packages/d5/a4/047d861fd8c538210e12b208c0479912273f991356b6bdee7ea8356b07c9/frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", size = 321292 }, - { url = "https://files.pythonhosted.org/packages/c0/25/cfec8af758b4525676cabd36efcaf7102c1348a776c0d1ad046b8a7cdc65/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", size = 301569 }, - { url = "https://files.pythonhosted.org/packages/87/2f/0c819372fa9f0c07b153124bf58683b8d0ca7bb73ea5ccde9b9ef1745beb/frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", size = 313625 }, - { url = "https://files.pythonhosted.org/packages/50/5f/f0cf8b0fdedffdb76b3745aa13d5dbe404d63493cc211ce8250f2025307f/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", size = 312523 }, - { url = "https://files.pythonhosted.org/packages/e1/6c/38c49108491272d3e84125bbabf2c2d0b304899b52f49f0539deb26ad18d/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", size = 322657 }, - { url = "https://files.pythonhosted.org/packages/bd/4b/3bd3bad5be06a9d1b04b1c22be80b5fe65b502992d62fab4bdb25d9366ee/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", size = 303414 }, - { url = "https://files.pythonhosted.org/packages/5b/89/7e225a30bef6e85dbfe22622c24afe932e9444de3b40d58b1ea589a14ef8/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", size = 320321 }, - { url = "https://files.pythonhosted.org/packages/22/72/7e3acef4dd9e86366cb8f4d8f28e852c2b7e116927e9722b31a6f71ea4b0/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", size = 323975 }, - { url = "https://files.pythonhosted.org/packages/d8/85/e5da03d20507e13c66ce612c9792b76811b7a43e3320cce42d95b85ac755/frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", size = 316553 }, - { url = "https://files.pythonhosted.org/packages/ac/8e/6c609cbd0580ae8a0661c408149f196aade7d325b1ae7adc930501b81acb/frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", size = 115511 }, - { url = "https://files.pythonhosted.org/packages/f2/13/a84804cfde6de12d44ed48ecbf777ba62b12ff09e761f76cdd1ff9e14bb1/frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", size = 120863 }, - { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193 }, - { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831 }, - { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862 }, - { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361 }, - { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115 }, - { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505 }, - { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666 }, - { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119 }, - { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226 }, - { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788 }, - { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914 }, - { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283 }, - { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264 }, - { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482 }, - { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248 }, - { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161 }, - { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548 }, - { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404 }, +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] [[package]] name = "fsspec" version = "2025.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033 } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052 }, + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, ] [[package]] name = "future" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 }, + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, ] [[package]] @@ -1893,24 +1924,24 @@ dependencies = [ { name = "zope-event" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/75/a53f1cb732420f5e5d79b2563fc3504d22115e7ecfe7966e5cf9b3582ae7/gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca", size = 5976624 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/a53f1cb732420f5e5d79b2563fc3504d22115e7ecfe7966e5cf9b3582ae7/gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca", size = 5976624, upload-time = "2024-11-11T15:36:45.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/fd/86a170f77ef51a15297573c50dbec4cc67ddc98b677cc2d03cc7f2927f4c/gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62", size = 2951424 }, - { url = "https://files.pythonhosted.org/packages/7f/0a/987268c9d446f61883bc627c77c5ed4a97869c0f541f76661a62b2c411f6/gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab", size = 4878504 }, - { url = "https://files.pythonhosted.org/packages/dc/d4/2f77ddd837c0e21b4a4460bcb79318b6754d95ef138b7a29f3221c7e9993/gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758", size = 5007668 }, - { url = "https://files.pythonhosted.org/packages/80/a0/829e0399a1f9b84c344b72d2be9aa60fe2a64e993cac221edcc14f069679/gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d", size = 5067055 }, - { url = "https://files.pythonhosted.org/packages/1e/67/0e693f9ddb7909c2414f8fcfc2409aa4157884c147bc83dab979e9cf717c/gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546", size = 6761883 }, - { url = "https://files.pythonhosted.org/packages/fa/b6/b69883fc069d7148dd23c5dda20826044e54e7197f3c8e72b8cc2cd4035a/gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c", size = 5440802 }, - { url = "https://files.pythonhosted.org/packages/32/4e/b00094d995ff01fd88b3cf6b9d1d794f935c31c645c431e65cd82d808c9c/gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61", size = 6866992 }, - { url = "https://files.pythonhosted.org/packages/37/ed/58dbe9fb09d36f6477ff8db0459ebd3be9a77dc05ae5d96dc91ad657610d/gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897", size = 1543736 }, - { url = "https://files.pythonhosted.org/packages/dd/32/301676f67ffa996ff1c4175092fb0c48c83271cc95e5c67650b87156b6cf/gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46", size = 2956467 }, - { url = "https://files.pythonhosted.org/packages/6b/84/aef1a598123cef2375b6e2bf9d17606b961040f8a10e3dcc3c3dd2a99f05/gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb", size = 5136486 }, - { url = "https://files.pythonhosted.org/packages/92/7b/04f61187ee1df7a913b3fca63b0a1206c29141ab4d2a57e7645237b6feb5/gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85", size = 5299718 }, - { url = "https://files.pythonhosted.org/packages/36/2a/ebd12183ac25eece91d084be2111e582b061f4d15ead32239b43ed47e9ba/gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6", size = 5400118 }, - { url = "https://files.pythonhosted.org/packages/ec/c9/f006c0cd59f0720fbb62ee11da0ad4c4c0fd12799afd957dd491137e80d9/gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671", size = 6775163 }, - { url = "https://files.pythonhosted.org/packages/49/f1/5edf00b674b10d67e3b967c2d46b8a124c2bc8cfd59d4722704392206444/gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f", size = 5479886 }, - { url = "https://files.pythonhosted.org/packages/22/11/c48e62744a32c0d48984268ae62b99edb81eaf0e03b42de52e2f09855509/gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a", size = 6891452 }, - { url = "https://files.pythonhosted.org/packages/11/b2/5d20664ef6a077bec9f27f7a7ee761edc64946d0b1e293726a3d074a9a18/gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae", size = 1541631 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/86a170f77ef51a15297573c50dbec4cc67ddc98b677cc2d03cc7f2927f4c/gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62", size = 2951424, upload-time = "2024-11-11T14:32:36.451Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/987268c9d446f61883bc627c77c5ed4a97869c0f541f76661a62b2c411f6/gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab", size = 4878504, upload-time = "2024-11-11T15:20:03.521Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d4/2f77ddd837c0e21b4a4460bcb79318b6754d95ef138b7a29f3221c7e9993/gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758", size = 5007668, upload-time = "2024-11-11T15:21:00.422Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/829e0399a1f9b84c344b72d2be9aa60fe2a64e993cac221edcc14f069679/gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d", size = 5067055, upload-time = "2024-11-11T15:22:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/1e/67/0e693f9ddb7909c2414f8fcfc2409aa4157884c147bc83dab979e9cf717c/gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546", size = 6761883, upload-time = "2024-11-11T14:57:09.359Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b6/b69883fc069d7148dd23c5dda20826044e54e7197f3c8e72b8cc2cd4035a/gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c", size = 5440802, upload-time = "2024-11-11T15:37:04.983Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/b00094d995ff01fd88b3cf6b9d1d794f935c31c645c431e65cd82d808c9c/gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61", size = 6866992, upload-time = "2024-11-11T15:03:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/58dbe9fb09d36f6477ff8db0459ebd3be9a77dc05ae5d96dc91ad657610d/gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897", size = 1543736, upload-time = "2024-11-11T15:03:06.121Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/301676f67ffa996ff1c4175092fb0c48c83271cc95e5c67650b87156b6cf/gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46", size = 2956467, upload-time = "2024-11-11T14:32:33.238Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/aef1a598123cef2375b6e2bf9d17606b961040f8a10e3dcc3c3dd2a99f05/gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb", size = 5136486, upload-time = "2024-11-11T15:20:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/7b/04f61187ee1df7a913b3fca63b0a1206c29141ab4d2a57e7645237b6feb5/gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85", size = 5299718, upload-time = "2024-11-11T15:21:03.354Z" }, + { url = "https://files.pythonhosted.org/packages/36/2a/ebd12183ac25eece91d084be2111e582b061f4d15ead32239b43ed47e9ba/gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6", size = 5400118, upload-time = "2024-11-11T15:22:45.897Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c9/f006c0cd59f0720fbb62ee11da0ad4c4c0fd12799afd957dd491137e80d9/gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671", size = 6775163, upload-time = "2024-11-11T14:57:11.991Z" }, + { url = "https://files.pythonhosted.org/packages/49/f1/5edf00b674b10d67e3b967c2d46b8a124c2bc8cfd59d4722704392206444/gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f", size = 5479886, upload-time = "2024-11-11T15:37:06.558Z" }, + { url = "https://files.pythonhosted.org/packages/22/11/c48e62744a32c0d48984268ae62b99edb81eaf0e03b42de52e2f09855509/gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a", size = 6891452, upload-time = "2024-11-11T15:03:46.892Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/5d20664ef6a077bec9f27f7a7ee761edc64946d0b1e293726a3d074a9a18/gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae", size = 1541631, upload-time = "2024-11-11T14:55:34.977Z" }, ] [[package]] @@ -1920,9 +1951,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] @@ -1932,31 +1963,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, ] [[package]] name = "gmpy2" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/bd/c6c154ce734a3e6187871b323297d8e5f3bdf9feaafc5212381538bc19e4/gmpy2-2.2.1.tar.gz", hash = "sha256:e83e07567441b78cb87544910cb3cc4fe94e7da987e93ef7622e76fb96650432", size = 234228 } +sdist = { url = "https://files.pythonhosted.org/packages/07/bd/c6c154ce734a3e6187871b323297d8e5f3bdf9feaafc5212381538bc19e4/gmpy2-2.2.1.tar.gz", hash = "sha256:e83e07567441b78cb87544910cb3cc4fe94e7da987e93ef7622e76fb96650432", size = 234228, upload-time = "2024-07-21T05:33:00.715Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/ec/ab67751ac0c4088ed21cf9a2a7f9966bf702ca8ebfc3204879cf58c90179/gmpy2-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98e947491c67523d3147a500f377bb64d0b115e4ab8a12d628fb324bb0e142bf", size = 880346 }, - { url = "https://files.pythonhosted.org/packages/97/7c/bdc4a7a2b0e543787a9354e80fdcf846c4e9945685218cef4ca938d25594/gmpy2-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ccd319a3a87529484167ae1391f937ac4a8724169fd5822bbb541d1eab612b0", size = 694518 }, - { url = "https://files.pythonhosted.org/packages/fc/44/ea903003bb4c3af004912fb0d6488e346bd76968f11a7472a1e60dee7dd7/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827bcd433e5d62f1b732f45e6949419da4a53915d6c80a3c7a5a03d5a783a03a", size = 1653491 }, - { url = "https://files.pythonhosted.org/packages/c9/70/5bce281b7cd664c04f1c9d47a37087db37b2be887bce738340e912ad86c8/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7131231fc96f57272066295c81cbf11b3233a9471659bca29ddc90a7bde9bfa", size = 1706487 }, - { url = "https://files.pythonhosted.org/packages/2a/52/1f773571f21cf0319fc33218a1b384f29de43053965c05ed32f7e6729115/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1cc6f2bb68ee00c20aae554e111dc781a76140e00c31e4eda5c8f2d4168ed06c", size = 1637415 }, - { url = "https://files.pythonhosted.org/packages/99/4c/390daf67c221b3f4f10b5b7d9293e61e4dbd48956a38947679c5a701af27/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae388fe46e3d20af4675451a4b6c12fc1bb08e6e0e69ee47072638be21bf42d8", size = 1657781 }, - { url = "https://files.pythonhosted.org/packages/61/cd/86e47bccb3636389e29c4654a0e5ac52926d832897f2f64632639b63ffc1/gmpy2-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8b472ee3c123b77979374da2293ebf2c170b88212e173d64213104956d4678fb", size = 1203346 }, - { url = "https://files.pythonhosted.org/packages/9a/ee/8f9f65e2bac334cfe13b3fc3f8962d5fc2858ebcf4517690d2d24afa6d0e/gmpy2-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d03a1be1b1ad3944013fae5250316c3f4e6aec45ecdf189a5c7422d640004d", size = 885231 }, - { url = "https://files.pythonhosted.org/packages/07/1c/bf29f6bf8acd72c3cf85d04e7db1bb26dd5507ee2387770bb787bc54e2a5/gmpy2-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd09dd43d199908c1d1d501c5de842b3bf754f99b94af5b5ef0e26e3b716d2d5", size = 696569 }, - { url = "https://files.pythonhosted.org/packages/7c/cc/38d33eadeccd81b604a95b67d43c71b246793b7c441f1d7c3b41978cd1cf/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3232859fda3e96fd1aecd6235ae20476ed4506562bcdef6796a629b78bb96acd", size = 1655776 }, - { url = "https://files.pythonhosted.org/packages/96/8d/d017599d6db8e9b96d6e84ea5102c33525cb71c82876b1813a2ece5d94ec/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fba6f7cf43fb7f8474216701b5aaddfa5e6a06d560e88a67f814062934e863", size = 1707529 }, - { url = "https://files.pythonhosted.org/packages/d0/93/91b4a0af23ae4216fd7ebcfd955dcbe152c5ef170598aee421310834de0a/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b33cae533ede8173bc7d4bb855b388c5b636ca9f22a32c949f2eb7e0cc531b2", size = 1634195 }, - { url = "https://files.pythonhosted.org/packages/d7/ba/08ee99f19424cd33d5f0f17b2184e34d2fa886eebafcd3e164ccba15d9f2/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:954e7e1936c26e370ca31bbd49729ebeeb2006a8f9866b1e778ebb89add2e941", size = 1656779 }, - { url = "https://files.pythonhosted.org/packages/14/e1/7b32ae2b23c8363d87b7f4bbac9abe9a1f820c2417d2e99ca3b4afd9379b/gmpy2-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c929870137b20d9c3f7dd97f43615b2d2c1a2470e50bafd9a5eea2e844f462e9", size = 1204668 }, + { url = "https://files.pythonhosted.org/packages/ac/ec/ab67751ac0c4088ed21cf9a2a7f9966bf702ca8ebfc3204879cf58c90179/gmpy2-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98e947491c67523d3147a500f377bb64d0b115e4ab8a12d628fb324bb0e142bf", size = 880346, upload-time = "2024-07-21T05:31:25.531Z" }, + { url = "https://files.pythonhosted.org/packages/97/7c/bdc4a7a2b0e543787a9354e80fdcf846c4e9945685218cef4ca938d25594/gmpy2-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ccd319a3a87529484167ae1391f937ac4a8724169fd5822bbb541d1eab612b0", size = 694518, upload-time = "2024-07-21T05:31:27.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/44/ea903003bb4c3af004912fb0d6488e346bd76968f11a7472a1e60dee7dd7/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:827bcd433e5d62f1b732f45e6949419da4a53915d6c80a3c7a5a03d5a783a03a", size = 1653491, upload-time = "2024-07-21T05:31:29.968Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/5bce281b7cd664c04f1c9d47a37087db37b2be887bce738340e912ad86c8/gmpy2-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7131231fc96f57272066295c81cbf11b3233a9471659bca29ddc90a7bde9bfa", size = 1706487, upload-time = "2024-07-21T05:31:32.476Z" }, + { url = "https://files.pythonhosted.org/packages/2a/52/1f773571f21cf0319fc33218a1b384f29de43053965c05ed32f7e6729115/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1cc6f2bb68ee00c20aae554e111dc781a76140e00c31e4eda5c8f2d4168ed06c", size = 1637415, upload-time = "2024-07-21T05:31:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/99/4c/390daf67c221b3f4f10b5b7d9293e61e4dbd48956a38947679c5a701af27/gmpy2-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae388fe46e3d20af4675451a4b6c12fc1bb08e6e0e69ee47072638be21bf42d8", size = 1657781, upload-time = "2024-07-21T05:31:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/61/cd/86e47bccb3636389e29c4654a0e5ac52926d832897f2f64632639b63ffc1/gmpy2-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8b472ee3c123b77979374da2293ebf2c170b88212e173d64213104956d4678fb", size = 1203346, upload-time = "2024-07-21T05:31:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/8f9f65e2bac334cfe13b3fc3f8962d5fc2858ebcf4517690d2d24afa6d0e/gmpy2-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d03a1be1b1ad3944013fae5250316c3f4e6aec45ecdf189a5c7422d640004d", size = 885231, upload-time = "2024-07-21T05:31:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/07/1c/bf29f6bf8acd72c3cf85d04e7db1bb26dd5507ee2387770bb787bc54e2a5/gmpy2-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd09dd43d199908c1d1d501c5de842b3bf754f99b94af5b5ef0e26e3b716d2d5", size = 696569, upload-time = "2024-07-21T05:31:43.768Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/38d33eadeccd81b604a95b67d43c71b246793b7c441f1d7c3b41978cd1cf/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3232859fda3e96fd1aecd6235ae20476ed4506562bcdef6796a629b78bb96acd", size = 1655776, upload-time = "2024-07-21T05:31:46.272Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/d017599d6db8e9b96d6e84ea5102c33525cb71c82876b1813a2ece5d94ec/gmpy2-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fba6f7cf43fb7f8474216701b5aaddfa5e6a06d560e88a67f814062934e863", size = 1707529, upload-time = "2024-07-21T05:31:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/d0/93/91b4a0af23ae4216fd7ebcfd955dcbe152c5ef170598aee421310834de0a/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9b33cae533ede8173bc7d4bb855b388c5b636ca9f22a32c949f2eb7e0cc531b2", size = 1634195, upload-time = "2024-07-21T05:31:50.99Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/08ee99f19424cd33d5f0f17b2184e34d2fa886eebafcd3e164ccba15d9f2/gmpy2-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:954e7e1936c26e370ca31bbd49729ebeeb2006a8f9866b1e778ebb89add2e941", size = 1656779, upload-time = "2024-07-21T05:31:53.657Z" }, + { url = "https://files.pythonhosted.org/packages/14/e1/7b32ae2b23c8363d87b7f4bbac9abe9a1f820c2417d2e99ca3b4afd9379b/gmpy2-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c929870137b20d9c3f7dd97f43615b2d2c1a2470e50bafd9a5eea2e844f462e9", size = 1204668, upload-time = "2024-07-21T05:31:56.264Z" }, ] [[package]] @@ -1966,9 +1997,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/97/b49c69893cddea912c7a660a4b6102c6b02cd268f8c7162dd70b7c16f753/google-3.0.0.tar.gz", hash = "sha256:143530122ee5130509ad5e989f0512f7cb218b2d4eddbafbad40fd10e8d8ccbe", size = 44978 } +sdist = { url = "https://files.pythonhosted.org/packages/89/97/b49c69893cddea912c7a660a4b6102c6b02cd268f8c7162dd70b7c16f753/google-3.0.0.tar.gz", hash = "sha256:143530122ee5130509ad5e989f0512f7cb218b2d4eddbafbad40fd10e8d8ccbe", size = 44978, upload-time = "2020-07-11T14:50:45.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/35/17c9141c4ae21e9a29a43acdfd848e3e468a810517f862cad07977bf8fe9/google-3.0.0-py2.py3-none-any.whl", hash = "sha256:889cf695f84e4ae2c55fbc0cfdaf4c1e729417fa52ab1db0485202ba173e4935", size = 45258 }, + { url = "https://files.pythonhosted.org/packages/ac/35/17c9141c4ae21e9a29a43acdfd848e3e468a810517f862cad07977bf8fe9/google-3.0.0-py2.py3-none-any.whl", hash = "sha256:889cf695f84e4ae2c55fbc0cfdaf4c1e729417fa52ab1db0485202ba173e4935", size = 45258, upload-time = "2020-07-11T14:49:58.287Z" }, ] [[package]] @@ -1982,9 +2013,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/8f/ecd68579bd2bf5e9321df60dcdee6e575adf77fedacb1d8378760b2b16b6/google-api-core-2.18.0.tar.gz", hash = "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9", size = 148047 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/8f/ecd68579bd2bf5e9321df60dcdee6e575adf77fedacb1d8378760b2b16b6/google-api-core-2.18.0.tar.gz", hash = "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9", size = 148047, upload-time = "2024-03-21T20:16:56.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/75/59a3ad90d9b4ff5b3e0537611dbe885aeb96124521c9d35aa079f1e0f2c9/google_api_core-2.18.0-py3-none-any.whl", hash = "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6", size = 138293 }, + { url = "https://files.pythonhosted.org/packages/86/75/59a3ad90d9b4ff5b3e0537611dbe885aeb96124521c9d35aa079f1e0f2c9/google_api_core-2.18.0-py3-none-any.whl", hash = "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6", size = 138293, upload-time = "2024-03-21T20:16:53.645Z" }, ] [package.optional-dependencies] @@ -2004,9 +2035,9 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/8b/d990f947c261304a5c1599d45717d02c27d46af5f23e1fee5dc19c8fa79d/google-api-python-client-2.90.0.tar.gz", hash = "sha256:cbcb3ba8be37c6806676a49df16ac412077e5e5dc7fa967941eff977b31fba03", size = 10891311 } +sdist = { url = "https://files.pythonhosted.org/packages/35/8b/d990f947c261304a5c1599d45717d02c27d46af5f23e1fee5dc19c8fa79d/google-api-python-client-2.90.0.tar.gz", hash = "sha256:cbcb3ba8be37c6806676a49df16ac412077e5e5dc7fa967941eff977b31fba03", size = 10891311, upload-time = "2023-06-20T16:29:25.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/03/209b5c36a621ae644dc7d4743746cd3b38b18e133f8779ecaf6b95cc01ce/google_api_python_client-2.90.0-py2.py3-none-any.whl", hash = "sha256:4a41ffb7797d4f28e44635fb1e7076240b741c6493e7c3233c0e4421cec7c913", size = 11379891 }, + { url = "https://files.pythonhosted.org/packages/39/03/209b5c36a621ae644dc7d4743746cd3b38b18e133f8779ecaf6b95cc01ce/google_api_python_client-2.90.0-py2.py3-none-any.whl", hash = "sha256:4a41ffb7797d4f28e44635fb1e7076240b741c6493e7c3233c0e4421cec7c913", size = 11379891, upload-time = "2023-06-20T16:29:19.532Z" }, ] [[package]] @@ -2018,9 +2049,9 @@ dependencies = [ { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/b2/f14129111cfd61793609643a07ecb03651a71dd65c6974f63b0310ff4b45/google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", size = 244326 } +sdist = { url = "https://files.pythonhosted.org/packages/18/b2/f14129111cfd61793609643a07ecb03651a71dd65c6974f63b0310ff4b45/google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", size = 244326, upload-time = "2024-03-20T17:24:27.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/8d/ddbcf81ec751d8ee5fd18ac11ff38a0e110f39dfbf105e6d9db69d556dd0/google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415", size = 189186 }, + { url = "https://files.pythonhosted.org/packages/9e/8d/ddbcf81ec751d8ee5fd18ac11ff38a0e110f39dfbf105e6d9db69d556dd0/google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415", size = 189186, upload-time = "2024-03-20T17:24:24.292Z" }, ] [[package]] @@ -2031,9 +2062,9 @@ dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842 } +sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253 }, + { url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" }, ] [[package]] @@ -2053,9 +2084,9 @@ dependencies = [ { name = "pydantic" }, { name = "shapely" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/21/5930a1420f82bec246ae09e1b7cc8458544f3befe669193b33a7b5c0691c/google-cloud-aiplatform-1.49.0.tar.gz", hash = "sha256:e6e6d01079bb5def49e4be4db4d12b13c624b5c661079c869c13c855e5807429", size = 5766450 } +sdist = { url = "https://files.pythonhosted.org/packages/47/21/5930a1420f82bec246ae09e1b7cc8458544f3befe669193b33a7b5c0691c/google-cloud-aiplatform-1.49.0.tar.gz", hash = "sha256:e6e6d01079bb5def49e4be4db4d12b13c624b5c661079c869c13c855e5807429", size = 5766450, upload-time = "2024-04-29T17:25:31.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/6a/7d9e1c03c814e760361fe8b0ffd373ead4124ace66ed33bb16d526ae1ecf/google_cloud_aiplatform-1.49.0-py2.py3-none-any.whl", hash = "sha256:8072d9e0c18d8942c704233d1a93b8d6312fc7b278786a283247950e28ae98df", size = 4914049 }, + { url = "https://files.pythonhosted.org/packages/39/6a/7d9e1c03c814e760361fe8b0ffd373ead4124ace66ed33bb16d526ae1ecf/google_cloud_aiplatform-1.49.0-py2.py3-none-any.whl", hash = "sha256:8072d9e0c18d8942c704233d1a93b8d6312fc7b278786a283247950e28ae98df", size = 4914049, upload-time = "2024-04-29T17:25:27.625Z" }, ] [[package]] @@ -2071,9 +2102,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/f9/e9da2d56d7028f05c0e2f5edf6ce43c773220c3172666c3dd925791d763d/google_cloud_bigquery-3.34.0.tar.gz", hash = "sha256:5ee1a78ba5c2ccb9f9a8b2bf3ed76b378ea68f49b6cac0544dc55cc97ff7c1ce", size = 489091 } +sdist = { url = "https://files.pythonhosted.org/packages/24/f9/e9da2d56d7028f05c0e2f5edf6ce43c773220c3172666c3dd925791d763d/google_cloud_bigquery-3.34.0.tar.gz", hash = "sha256:5ee1a78ba5c2ccb9f9a8b2bf3ed76b378ea68f49b6cac0544dc55cc97ff7c1ce", size = 489091, upload-time = "2025-05-29T17:18:06.03Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/7e/7115c4f67ca0bc678f25bff1eab56cc37d06eb9a3978940b2ebd0705aa0a/google_cloud_bigquery-3.34.0-py3-none-any.whl", hash = "sha256:de20ded0680f8136d92ff5256270b5920dfe4fae479f5d0f73e90e5df30b1cf7", size = 253555 }, + { url = "https://files.pythonhosted.org/packages/b1/7e/7115c4f67ca0bc678f25bff1eab56cc37d06eb9a3978940b2ebd0705aa0a/google_cloud_bigquery-3.34.0-py3-none-any.whl", hash = "sha256:de20ded0680f8136d92ff5256270b5920dfe4fae479f5d0f73e90e5df30b1cf7", size = 253555, upload-time = "2025-05-29T17:18:02.904Z" }, ] [[package]] @@ -2084,9 +2115,9 @@ dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/b8/2b53838d2acd6ec6168fd284a990c76695e84c65deee79c9f3a4276f6b4f/google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53", size = 35861, upload-time = "2025-03-10T21:05:38.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348 }, + { url = "https://files.pythonhosted.org/packages/40/86/bda7241a8da2d28a754aad2ba0f6776e35b67e37c36ae0c45d49370f1014/google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e", size = 29348, upload-time = "2025-03-10T21:05:37.785Z" }, ] [[package]] @@ -2100,9 +2131,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/ca/a4648f5038cb94af4b3942815942a03aa9398f9fb0bef55b3f1585b9940d/google_cloud_resource_manager-1.14.2.tar.gz", hash = "sha256:962e2d904c550d7bac48372607904ff7bb3277e3bb4a36d80cc9a37e28e6eb74", size = 446370 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/ca/a4648f5038cb94af4b3942815942a03aa9398f9fb0bef55b3f1585b9940d/google_cloud_resource_manager-1.14.2.tar.gz", hash = "sha256:962e2d904c550d7bac48372607904ff7bb3277e3bb4a36d80cc9a37e28e6eb74", size = 446370, upload-time = "2025-03-17T11:35:56.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/ea/a92631c358da377af34d3a9682c97af83185c2d66363d5939ab4a1169a7f/google_cloud_resource_manager-1.14.2-py3-none-any.whl", hash = "sha256:d0fa954dedd1d2b8e13feae9099c01b8aac515b648e612834f9942d2795a9900", size = 394344 }, + { url = "https://files.pythonhosted.org/packages/b1/ea/a92631c358da377af34d3a9682c97af83185c2d66363d5939ab4a1169a7f/google_cloud_resource_manager-1.14.2-py3-none-any.whl", hash = "sha256:d0fa954dedd1d2b8e13feae9099c01b8aac515b648e612834f9942d2795a9900", size = 394344, upload-time = "2025-03-17T11:35:54.722Z" }, ] [[package]] @@ -2117,29 +2148,29 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/c5/0bc3f97cf4c14a731ecc5a95c5cde6883aec7289dc74817f9b41f866f77e/google-cloud-storage-2.16.0.tar.gz", hash = "sha256:dda485fa503710a828d01246bd16ce9db0823dc51bbca742ce96a6817d58669f", size = 5525307 } +sdist = { url = "https://files.pythonhosted.org/packages/17/c5/0bc3f97cf4c14a731ecc5a95c5cde6883aec7289dc74817f9b41f866f77e/google-cloud-storage-2.16.0.tar.gz", hash = "sha256:dda485fa503710a828d01246bd16ce9db0823dc51bbca742ce96a6817d58669f", size = 5525307, upload-time = "2024-03-18T23:55:37.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/e5/7d045d188f4ef85d94b9e3ae1bf876170c6b9f4c9a950124978efc36f680/google_cloud_storage-2.16.0-py2.py3-none-any.whl", hash = "sha256:91a06b96fb79cf9cdfb4e759f178ce11ea885c79938f89590344d079305f5852", size = 125604 }, + { url = "https://files.pythonhosted.org/packages/cb/e5/7d045d188f4ef85d94b9e3ae1bf876170c6b9f4c9a950124978efc36f680/google_cloud_storage-2.16.0-py2.py3-none-any.whl", hash = "sha256:91a06b96fb79cf9cdfb4e759f178ce11ea885c79938f89590344d079305f5852", size = 125604, upload-time = "2024-03-18T23:55:33.987Z" }, ] [[package]] name = "google-crc32c" version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495 } +sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495, upload-time = "2025-03-26T14:29:13.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468 }, - { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313 }, - { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048 }, - { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669 }, - { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476 }, - { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470 }, - { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315 }, - { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180 }, - { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794 }, - { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477 }, - { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241 }, - { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048 }, + { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468, upload-time = "2025-03-26T14:32:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313, upload-time = "2025-03-26T14:57:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048, upload-time = "2025-03-26T14:41:30.679Z" }, + { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669, upload-time = "2025-03-26T14:41:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476, upload-time = "2025-03-26T14:29:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470, upload-time = "2025-03-26T14:34:31.655Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315, upload-time = "2025-03-26T15:01:54.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180, upload-time = "2025-03-26T14:41:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794, upload-time = "2025-03-26T14:41:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477, upload-time = "2025-03-26T14:29:10.94Z" }, + { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241, upload-time = "2025-03-26T14:41:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048, upload-time = "2025-03-26T14:41:46.696Z" }, ] [[package]] @@ -2149,9 +2180,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099 } +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099, upload-time = "2024-08-07T22:20:38.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251 }, + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251, upload-time = "2024-08-07T22:20:36.409Z" }, ] [[package]] @@ -2161,9 +2192,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/dc/291cebf3c73e108ef8210f19cb83d671691354f4f7dd956445560d778715/googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e", size = 121646 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/dc/291cebf3c73e108ef8210f19cb83d671691354f4f7dd956445560d778715/googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e", size = 121646, upload-time = "2024-03-11T12:33:15.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/a6/12a0c976140511d8bc8a16ad15793b2aef29ac927baa0786ccb7ddbb6e1c/googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632", size = 229141 }, + { url = "https://files.pythonhosted.org/packages/dc/a6/12a0c976140511d8bc8a16ad15793b2aef29ac927baa0786ccb7ddbb6e1c/googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632", size = 229141, upload-time = "2024-03-11T12:33:14.052Z" }, ] [package.optional-dependencies] @@ -2179,9 +2210,9 @@ dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/9c/62c3241731b59c1c403377abef17b5e3782f6385b0317f6d7083271db501/gotrue-2.11.4.tar.gz", hash = "sha256:a9ced242b16c6d6bedc43bca21bbefea1ba5fb35fcdaad7d529342099d3b1767", size = 35353 } +sdist = { url = "https://files.pythonhosted.org/packages/19/9c/62c3241731b59c1c403377abef17b5e3782f6385b0317f6d7083271db501/gotrue-2.11.4.tar.gz", hash = "sha256:a9ced242b16c6d6bedc43bca21bbefea1ba5fb35fcdaad7d529342099d3b1767", size = 35353, upload-time = "2025-02-20T09:02:37.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/3a/1a7cac16438f4e5319a0c879416d5e5032c98c3db2874e6e5300b3b475e6/gotrue-2.11.4-py3-none-any.whl", hash = "sha256:712e5018acc00d93cfc6d7bfddc3114eb3c420ab03b945757a8ba38c5fc3caa8", size = 41106 }, + { url = "https://files.pythonhosted.org/packages/47/3a/1a7cac16438f4e5319a0c879416d5e5032c98c3db2874e6e5300b3b475e6/gotrue-2.11.4-py3-none-any.whl", hash = "sha256:712e5018acc00d93cfc6d7bfddc3114eb3c420ab03b945757a8ba38c5fc3caa8", size = 41106, upload-time = "2025-02-20T09:02:34.653Z" }, ] [[package]] @@ -2194,9 +2225,9 @@ dependencies = [ { name = "graphql-core" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/ed/44ffd30b06b3afc8274ee2f38c3c1b61fe4740bf03d92083e43d2c17ac77/gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b", size = 180504 } +sdist = { url = "https://files.pythonhosted.org/packages/34/ed/44ffd30b06b3afc8274ee2f38c3c1b61fe4740bf03d92083e43d2c17ac77/gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b", size = 180504, upload-time = "2025-05-20T12:34:08.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/50/2f4e99b216821ac921dbebf91c644ba95818f5d07857acadee17220221f3/gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc", size = 74348 }, + { url = "https://files.pythonhosted.org/packages/cb/50/2f4e99b216821ac921dbebf91c644ba95818f5d07857acadee17220221f3/gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc", size = 74348, upload-time = "2025-05-20T12:34:07.687Z" }, ] [package.optional-dependencies] @@ -2212,35 +2243,35 @@ requests = [ name = "graphql-core" version = "3.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353, upload-time = "2025-01-26T16:36:27.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 }, + { url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416, upload-time = "2025-01-26T16:36:24.868Z" }, ] [[package]] name = "greenlet" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/9f/a47e19261747b562ce88219e5ed8c859d42c6e01e73da6fbfa3f08a7be13/greenlet-3.2.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:dcb9cebbf3f62cb1e5afacae90761ccce0effb3adaa32339a0670fe7805d8068", size = 268635 }, - { url = "https://files.pythonhosted.org/packages/11/80/a0042b91b66975f82a914d515e81c1944a3023f2ce1ed7a9b22e10b46919/greenlet-3.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf3fc9145141250907730886b031681dfcc0de1c158f3cc51c092223c0f381ce", size = 628786 }, - { url = "https://files.pythonhosted.org/packages/38/a2/8336bf1e691013f72a6ebab55da04db81a11f68e82bb691f434909fa1327/greenlet-3.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efcdfb9df109e8a3b475c016f60438fcd4be68cd13a365d42b35914cdab4bb2b", size = 640866 }, - { url = "https://files.pythonhosted.org/packages/f8/7e/f2a3a13e424670a5d08826dab7468fa5e403e0fbe0b5f951ff1bc4425b45/greenlet-3.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd139e4943547ce3a56ef4b8b1b9479f9e40bb47e72cc906f0f66b9d0d5cab3", size = 636752 }, - { url = "https://files.pythonhosted.org/packages/fd/5d/ce4a03a36d956dcc29b761283f084eb4a3863401c7cb505f113f73af8774/greenlet-3.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71566302219b17ca354eb274dfd29b8da3c268e41b646f330e324e3967546a74", size = 636028 }, - { url = "https://files.pythonhosted.org/packages/4b/29/b130946b57e3ceb039238413790dd3793c5e7b8e14a54968de1fe449a7cf/greenlet-3.2.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3091bc45e6b0c73f225374fefa1536cd91b1e987377b12ef5b19129b07d93ebe", size = 583869 }, - { url = "https://files.pythonhosted.org/packages/ac/30/9f538dfe7f87b90ecc75e589d20cbd71635531a617a336c386d775725a8b/greenlet-3.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:44671c29da26539a5f142257eaba5110f71887c24d40df3ac87f1117df589e0e", size = 1112886 }, - { url = "https://files.pythonhosted.org/packages/be/92/4b7deeb1a1e9c32c1b59fdca1cac3175731c23311ddca2ea28a8b6ada91c/greenlet-3.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c23ea227847c9dbe0b3910f5c0dd95658b607137614eb821e6cbaecd60d81cc6", size = 1138355 }, - { url = "https://files.pythonhosted.org/packages/c5/eb/7551c751a2ea6498907b2fcbe31d7a54b602ba5e8eb9550a9695ca25d25c/greenlet-3.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0a16fb934fcabfdfacf21d79e6fed81809d8cd97bc1be9d9c89f0e4567143d7b", size = 295437 }, - { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413 }, - { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242 }, - { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067 }, - { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153 }, - { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865 }, - { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575 }, - { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460 }, - { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239 }, +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, ] [[package]] @@ -2252,35 +2283,35 @@ dependencies = [ { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/4e/8d0ca3b035e41fe0b3f31ebbb638356af720335e5a11154c330169b40777/grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20", size = 16259 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/4e/8d0ca3b035e41fe0b3f31ebbb638356af720335e5a11154c330169b40777/grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20", size = 16259, upload-time = "2025-03-17T11:40:23.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/6f/dd9b178aee7835b96c2e63715aba6516a9d50f6bebbd1cc1d32c82a2a6c3/grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351", size = 19242 }, + { url = "https://files.pythonhosted.org/packages/66/6f/dd9b178aee7835b96c2e63715aba6516a9d50f6bebbd1cc1d32c82a2a6c3/grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351", size = 19242, upload-time = "2025-03-17T11:40:22.648Z" }, ] [[package]] name = "grpcio" version = "1.67.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/53/d9282a66a5db45981499190b77790570617a604a38f3d103d0400974aeb5/grpcio-1.67.1.tar.gz", hash = "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", size = 12580022 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/2c/b60d6ea1f63a20a8d09c6db95c4f9a16497913fb3048ce0990ed81aeeca0/grpcio-1.67.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:7818c0454027ae3384235a65210bbf5464bd715450e30a3d40385453a85a70cb", size = 5119075 }, - { url = "https://files.pythonhosted.org/packages/b3/9a/e1956f7ca582a22dd1f17b9e26fcb8229051b0ce6d33b47227824772feec/grpcio-1.67.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ea33986b70f83844cd00814cee4451055cd8cab36f00ac64a31f5bb09b31919e", size = 11009159 }, - { url = "https://files.pythonhosted.org/packages/43/a8/35fbbba580c4adb1d40d12e244cf9f7c74a379073c0a0ca9d1b5338675a1/grpcio-1.67.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c7a01337407dd89005527623a4a72c5c8e2894d22bead0895306b23c6695698f", size = 5629476 }, - { url = "https://files.pythonhosted.org/packages/77/c9/864d336e167263d14dfccb4dbfa7fce634d45775609895287189a03f1fc3/grpcio-1.67.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b866f73224b0634f4312a4674c1be21b2b4afa73cb20953cbbb73a6b36c3cc", size = 6239901 }, - { url = "https://files.pythonhosted.org/packages/f7/1e/0011408ebabf9bd69f4f87cc1515cbfe2094e5a32316f8714a75fd8ddfcb/grpcio-1.67.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", size = 5881010 }, - { url = "https://files.pythonhosted.org/packages/b4/7d/fbca85ee9123fb296d4eff8df566f458d738186d0067dec6f0aa2fd79d71/grpcio-1.67.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8a23cbcc5bb11ea7dc6163078be36c065db68d915c24f5faa4f872c573bb400f", size = 6580706 }, - { url = "https://files.pythonhosted.org/packages/75/7a/766149dcfa2dfa81835bf7df623944c1f636a15fcb9b6138ebe29baf0bc6/grpcio-1.67.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1a65b503d008f066e994f34f456e0647e5ceb34cfcec5ad180b1b44020ad4970", size = 6161799 }, - { url = "https://files.pythonhosted.org/packages/09/13/5b75ae88810aaea19e846f5380611837de411181df51fd7a7d10cb178dcb/grpcio-1.67.1-cp311-cp311-win32.whl", hash = "sha256:e29ca27bec8e163dca0c98084040edec3bc49afd10f18b412f483cc68c712744", size = 3616330 }, - { url = "https://files.pythonhosted.org/packages/aa/39/38117259613f68f072778c9638a61579c0cfa5678c2558706b10dd1d11d3/grpcio-1.67.1-cp311-cp311-win_amd64.whl", hash = "sha256:786a5b18544622bfb1e25cc08402bd44ea83edfb04b93798d85dca4d1a0b5be5", size = 4354535 }, - { url = "https://files.pythonhosted.org/packages/6e/25/6f95bd18d5f506364379eabc0d5874873cc7dbdaf0757df8d1e82bc07a88/grpcio-1.67.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:267d1745894200e4c604958da5f856da6293f063327cb049a51fe67348e4f953", size = 5089809 }, - { url = "https://files.pythonhosted.org/packages/10/3f/d79e32e5d0354be33a12db2267c66d3cfeff700dd5ccdd09fd44a3ff4fb6/grpcio-1.67.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:85f69fdc1d28ce7cff8de3f9c67db2b0ca9ba4449644488c1e0303c146135ddb", size = 10981985 }, - { url = "https://files.pythonhosted.org/packages/21/f2/36fbc14b3542e3a1c20fb98bd60c4732c55a44e374a4eb68f91f28f14aab/grpcio-1.67.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f26b0b547eb8d00e195274cdfc63ce64c8fc2d3e2d00b12bf468ece41a0423a0", size = 5588770 }, - { url = "https://files.pythonhosted.org/packages/0d/af/bbc1305df60c4e65de8c12820a942b5e37f9cf684ef5e49a63fbb1476a73/grpcio-1.67.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4422581cdc628f77302270ff839a44f4c24fdc57887dc2a45b7e53d8fc2376af", size = 6214476 }, - { url = "https://files.pythonhosted.org/packages/92/cf/1d4c3e93efa93223e06a5c83ac27e32935f998bc368e276ef858b8883154/grpcio-1.67.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7616d2ded471231c701489190379e0c311ee0a6c756f3c03e6a62b95a7146e", size = 5850129 }, - { url = "https://files.pythonhosted.org/packages/ae/ca/26195b66cb253ac4d5ef59846e354d335c9581dba891624011da0e95d67b/grpcio-1.67.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8a00efecde9d6fcc3ab00c13f816313c040a28450e5e25739c24f432fc6d3c75", size = 6568489 }, - { url = "https://files.pythonhosted.org/packages/d1/94/16550ad6b3f13b96f0856ee5dfc2554efac28539ee84a51d7b14526da985/grpcio-1.67.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", size = 6149369 }, - { url = "https://files.pythonhosted.org/packages/33/0d/4c3b2587e8ad7f121b597329e6c2620374fccbc2e4e1aa3c73ccc670fde4/grpcio-1.67.1-cp312-cp312-win32.whl", hash = "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", size = 3599176 }, - { url = "https://files.pythonhosted.org/packages/7d/36/0c03e2d80db69e2472cf81c6123aa7d14741de7cf790117291a703ae6ae1/grpcio-1.67.1-cp312-cp312-win_amd64.whl", hash = "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", size = 4346574 }, +sdist = { url = "https://files.pythonhosted.org/packages/20/53/d9282a66a5db45981499190b77790570617a604a38f3d103d0400974aeb5/grpcio-1.67.1.tar.gz", hash = "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", size = 12580022, upload-time = "2024-10-29T06:30:07.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/2c/b60d6ea1f63a20a8d09c6db95c4f9a16497913fb3048ce0990ed81aeeca0/grpcio-1.67.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:7818c0454027ae3384235a65210bbf5464bd715450e30a3d40385453a85a70cb", size = 5119075, upload-time = "2024-10-29T06:24:04.696Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/e1956f7ca582a22dd1f17b9e26fcb8229051b0ce6d33b47227824772feec/grpcio-1.67.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ea33986b70f83844cd00814cee4451055cd8cab36f00ac64a31f5bb09b31919e", size = 11009159, upload-time = "2024-10-29T06:24:07.781Z" }, + { url = "https://files.pythonhosted.org/packages/43/a8/35fbbba580c4adb1d40d12e244cf9f7c74a379073c0a0ca9d1b5338675a1/grpcio-1.67.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c7a01337407dd89005527623a4a72c5c8e2894d22bead0895306b23c6695698f", size = 5629476, upload-time = "2024-10-29T06:24:11.444Z" }, + { url = "https://files.pythonhosted.org/packages/77/c9/864d336e167263d14dfccb4dbfa7fce634d45775609895287189a03f1fc3/grpcio-1.67.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b866f73224b0634f4312a4674c1be21b2b4afa73cb20953cbbb73a6b36c3cc", size = 6239901, upload-time = "2024-10-29T06:24:14.2Z" }, + { url = "https://files.pythonhosted.org/packages/f7/1e/0011408ebabf9bd69f4f87cc1515cbfe2094e5a32316f8714a75fd8ddfcb/grpcio-1.67.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", size = 5881010, upload-time = "2024-10-29T06:24:17.451Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7d/fbca85ee9123fb296d4eff8df566f458d738186d0067dec6f0aa2fd79d71/grpcio-1.67.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8a23cbcc5bb11ea7dc6163078be36c065db68d915c24f5faa4f872c573bb400f", size = 6580706, upload-time = "2024-10-29T06:24:20.038Z" }, + { url = "https://files.pythonhosted.org/packages/75/7a/766149dcfa2dfa81835bf7df623944c1f636a15fcb9b6138ebe29baf0bc6/grpcio-1.67.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1a65b503d008f066e994f34f456e0647e5ceb34cfcec5ad180b1b44020ad4970", size = 6161799, upload-time = "2024-10-29T06:24:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/5b75ae88810aaea19e846f5380611837de411181df51fd7a7d10cb178dcb/grpcio-1.67.1-cp311-cp311-win32.whl", hash = "sha256:e29ca27bec8e163dca0c98084040edec3bc49afd10f18b412f483cc68c712744", size = 3616330, upload-time = "2024-10-29T06:24:25.775Z" }, + { url = "https://files.pythonhosted.org/packages/aa/39/38117259613f68f072778c9638a61579c0cfa5678c2558706b10dd1d11d3/grpcio-1.67.1-cp311-cp311-win_amd64.whl", hash = "sha256:786a5b18544622bfb1e25cc08402bd44ea83edfb04b93798d85dca4d1a0b5be5", size = 4354535, upload-time = "2024-10-29T06:24:28.614Z" }, + { url = "https://files.pythonhosted.org/packages/6e/25/6f95bd18d5f506364379eabc0d5874873cc7dbdaf0757df8d1e82bc07a88/grpcio-1.67.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:267d1745894200e4c604958da5f856da6293f063327cb049a51fe67348e4f953", size = 5089809, upload-time = "2024-10-29T06:24:31.24Z" }, + { url = "https://files.pythonhosted.org/packages/10/3f/d79e32e5d0354be33a12db2267c66d3cfeff700dd5ccdd09fd44a3ff4fb6/grpcio-1.67.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:85f69fdc1d28ce7cff8de3f9c67db2b0ca9ba4449644488c1e0303c146135ddb", size = 10981985, upload-time = "2024-10-29T06:24:34.942Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/36fbc14b3542e3a1c20fb98bd60c4732c55a44e374a4eb68f91f28f14aab/grpcio-1.67.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f26b0b547eb8d00e195274cdfc63ce64c8fc2d3e2d00b12bf468ece41a0423a0", size = 5588770, upload-time = "2024-10-29T06:24:38.145Z" }, + { url = "https://files.pythonhosted.org/packages/0d/af/bbc1305df60c4e65de8c12820a942b5e37f9cf684ef5e49a63fbb1476a73/grpcio-1.67.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4422581cdc628f77302270ff839a44f4c24fdc57887dc2a45b7e53d8fc2376af", size = 6214476, upload-time = "2024-10-29T06:24:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/92/cf/1d4c3e93efa93223e06a5c83ac27e32935f998bc368e276ef858b8883154/grpcio-1.67.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d7616d2ded471231c701489190379e0c311ee0a6c756f3c03e6a62b95a7146e", size = 5850129, upload-time = "2024-10-29T06:24:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ca/26195b66cb253ac4d5ef59846e354d335c9581dba891624011da0e95d67b/grpcio-1.67.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8a00efecde9d6fcc3ab00c13f816313c040a28450e5e25739c24f432fc6d3c75", size = 6568489, upload-time = "2024-10-29T06:24:46.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/94/16550ad6b3f13b96f0856ee5dfc2554efac28539ee84a51d7b14526da985/grpcio-1.67.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", size = 6149369, upload-time = "2024-10-29T06:24:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/4c3b2587e8ad7f121b597329e6c2620374fccbc2e4e1aa3c73ccc670fde4/grpcio-1.67.1-cp312-cp312-win32.whl", hash = "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", size = 3599176, upload-time = "2024-10-29T06:24:51.443Z" }, + { url = "https://files.pythonhosted.org/packages/7d/36/0c03e2d80db69e2472cf81c6123aa7d14741de7cf790117291a703ae6ae1/grpcio-1.67.1-cp312-cp312-win_amd64.whl", hash = "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", size = 4346574, upload-time = "2024-10-29T06:24:54.587Z" }, ] [[package]] @@ -2292,9 +2323,9 @@ dependencies = [ { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/d7/013ef01c5a1c2fd0932c27c904934162f69f41ca0f28396d3ffe4d386123/grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485", size = 13063 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/d7/013ef01c5a1c2fd0932c27c904934162f69f41ca0f28396d3ffe4d386123/grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485", size = 13063, upload-time = "2024-08-06T00:37:08.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/40/972271de05f9315c0d69f9f7ebbcadd83bc85322f538637d11bb8c67803d/grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8", size = 14448 }, + { url = "https://files.pythonhosted.org/packages/90/40/972271de05f9315c0d69f9f7ebbcadd83bc85322f538637d11bb8c67803d/grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8", size = 14448, upload-time = "2024-08-06T00:30:15.702Z" }, ] [[package]] @@ -2306,24 +2337,24 @@ dependencies = [ { name = "protobuf" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/fa/b69bd8040eafc09b88bb0ec0fea59e8aacd1a801e688af087cead213b0d0/grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833", size = 4538520 } +sdist = { url = "https://files.pythonhosted.org/packages/54/fa/b69bd8040eafc09b88bb0ec0fea59e8aacd1a801e688af087cead213b0d0/grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833", size = 4538520, upload-time = "2024-08-06T00:37:11.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/52/2dfe0a46b63f5ebcd976570aa5fc62f793d5a8b169e211c6a5aede72b7ae/grpcio_tools-1.62.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23", size = 5147623 }, - { url = "https://files.pythonhosted.org/packages/f0/2e/29fdc6c034e058482e054b4a3c2432f84ff2e2765c1342d4f0aa8a5c5b9a/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492", size = 2719538 }, - { url = "https://files.pythonhosted.org/packages/f9/60/abe5deba32d9ec2c76cdf1a2f34e404c50787074a2fee6169568986273f1/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7", size = 3070964 }, - { url = "https://files.pythonhosted.org/packages/bc/ad/e2b066684c75f8d9a48508cde080a3a36618064b9cadac16d019ca511444/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43", size = 2805003 }, - { url = "https://files.pythonhosted.org/packages/9c/3f/59bf7af786eae3f9d24ee05ce75318b87f541d0950190ecb5ffb776a1a58/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a", size = 3685154 }, - { url = "https://files.pythonhosted.org/packages/f1/79/4dd62478b91e27084c67b35a2316ce8a967bd8b6cb8d6ed6c86c3a0df7cb/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3", size = 3297942 }, - { url = "https://files.pythonhosted.org/packages/b8/cb/86449ecc58bea056b52c0b891f26977afc8c4464d88c738f9648da941a75/grpcio_tools-1.62.3-cp311-cp311-win32.whl", hash = "sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5", size = 910231 }, - { url = "https://files.pythonhosted.org/packages/45/a4/9736215e3945c30ab6843280b0c6e1bff502910156ea2414cd77fbf1738c/grpcio_tools-1.62.3-cp311-cp311-win_amd64.whl", hash = "sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f", size = 1052496 }, - { url = "https://files.pythonhosted.org/packages/2a/a5/d6887eba415ce318ae5005e8dfac3fa74892400b54b6d37b79e8b4f14f5e/grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5", size = 5147690 }, - { url = "https://files.pythonhosted.org/packages/8a/7c/3cde447a045e83ceb4b570af8afe67ffc86896a2fe7f59594dc8e5d0a645/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133", size = 2720538 }, - { url = "https://files.pythonhosted.org/packages/88/07/f83f2750d44ac4f06c07c37395b9c1383ef5c994745f73c6bfaf767f0944/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa", size = 3071571 }, - { url = "https://files.pythonhosted.org/packages/37/74/40175897deb61e54aca716bc2e8919155b48f33aafec8043dda9592d8768/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0", size = 2806207 }, - { url = "https://files.pythonhosted.org/packages/ec/ee/d8de915105a217cbcb9084d684abdc032030dcd887277f2ef167372287fe/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d", size = 3685815 }, - { url = "https://files.pythonhosted.org/packages/fd/d9/4360a6c12be3d7521b0b8c39e5d3801d622fbb81cc2721dbd3eee31e28c8/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc", size = 3298378 }, - { url = "https://files.pythonhosted.org/packages/29/3b/7cdf4a9e5a3e0a35a528b48b111355cd14da601413a4f887aa99b6da468f/grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b", size = 910416 }, - { url = "https://files.pythonhosted.org/packages/6c/66/dd3ec249e44c1cc15e902e783747819ed41ead1336fcba72bf841f72c6e9/grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7", size = 1052856 }, + { url = "https://files.pythonhosted.org/packages/23/52/2dfe0a46b63f5ebcd976570aa5fc62f793d5a8b169e211c6a5aede72b7ae/grpcio_tools-1.62.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23", size = 5147623, upload-time = "2024-08-06T00:30:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2e/29fdc6c034e058482e054b4a3c2432f84ff2e2765c1342d4f0aa8a5c5b9a/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492", size = 2719538, upload-time = "2024-08-06T00:30:57.928Z" }, + { url = "https://files.pythonhosted.org/packages/f9/60/abe5deba32d9ec2c76cdf1a2f34e404c50787074a2fee6169568986273f1/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7", size = 3070964, upload-time = "2024-08-06T00:31:00.267Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ad/e2b066684c75f8d9a48508cde080a3a36618064b9cadac16d019ca511444/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43", size = 2805003, upload-time = "2024-08-06T00:31:02.565Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3f/59bf7af786eae3f9d24ee05ce75318b87f541d0950190ecb5ffb776a1a58/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a", size = 3685154, upload-time = "2024-08-06T00:31:05.339Z" }, + { url = "https://files.pythonhosted.org/packages/f1/79/4dd62478b91e27084c67b35a2316ce8a967bd8b6cb8d6ed6c86c3a0df7cb/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3", size = 3297942, upload-time = "2024-08-06T00:31:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/86449ecc58bea056b52c0b891f26977afc8c4464d88c738f9648da941a75/grpcio_tools-1.62.3-cp311-cp311-win32.whl", hash = "sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5", size = 910231, upload-time = "2024-08-06T00:31:11.464Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/9736215e3945c30ab6843280b0c6e1bff502910156ea2414cd77fbf1738c/grpcio_tools-1.62.3-cp311-cp311-win_amd64.whl", hash = "sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f", size = 1052496, upload-time = "2024-08-06T00:31:13.665Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a5/d6887eba415ce318ae5005e8dfac3fa74892400b54b6d37b79e8b4f14f5e/grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5", size = 5147690, upload-time = "2024-08-06T00:31:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/3cde447a045e83ceb4b570af8afe67ffc86896a2fe7f59594dc8e5d0a645/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133", size = 2720538, upload-time = "2024-08-06T00:31:18.905Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/f83f2750d44ac4f06c07c37395b9c1383ef5c994745f73c6bfaf767f0944/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa", size = 3071571, upload-time = "2024-08-06T00:31:21.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/74/40175897deb61e54aca716bc2e8919155b48f33aafec8043dda9592d8768/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0", size = 2806207, upload-time = "2024-08-06T00:31:24.208Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/d8de915105a217cbcb9084d684abdc032030dcd887277f2ef167372287fe/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d", size = 3685815, upload-time = "2024-08-06T00:31:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/4360a6c12be3d7521b0b8c39e5d3801d622fbb81cc2721dbd3eee31e28c8/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc", size = 3298378, upload-time = "2024-08-06T00:31:30.401Z" }, + { url = "https://files.pythonhosted.org/packages/29/3b/7cdf4a9e5a3e0a35a528b48b111355cd14da601413a4f887aa99b6da468f/grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b", size = 910416, upload-time = "2024-08-06T00:31:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/dd3ec249e44c1cc15e902e783747819ed41ead1336fcba72bf841f72c6e9/grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7", size = 1052856, upload-time = "2024-08-06T00:31:36.519Z" }, ] [[package]] @@ -2333,18 +2364,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031 } +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029 }, + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -2355,71 +2386,71 @@ dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" }, ] [[package]] name = "hf-xet" -version = "1.1.2" +version = "1.1.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/be/58f20728a5b445f8b064e74f0618897b3439f5ef90934da1916b9dfac76f/hf_xet-1.1.2.tar.gz", hash = "sha256:3712d6d4819d3976a1c18e36db9f503e296283f9363af818f50703506ed63da3", size = 467009 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d4/7685999e85945ed0d7f0762b686ae7015035390de1161dcea9d5276c134c/hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694", size = 495969, upload-time = "2025-06-20T21:48:38.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/ae/f1a63f75d9886f18a80220ba31a1c7b9c4752f03aae452f358f538c6a991/hf_xet-1.1.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dfd1873fd648488c70735cb60f7728512bca0e459e61fcd107069143cd798469", size = 2642559 }, - { url = "https://files.pythonhosted.org/packages/50/ab/d2c83ae18f1015d926defd5bfbe94c62d15e93f900e6a192e318ee947105/hf_xet-1.1.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:29b584983b2d977c44157d9241dcf0fd50acde0b7bff8897fe4386912330090d", size = 2541360 }, - { url = "https://files.pythonhosted.org/packages/9f/a7/693dc9f34f979e30a378125e2150a0b2d8d166e6d83ce3950eeb81e560aa/hf_xet-1.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b29ac84298147fe9164cc55ad994ba47399f90b5d045b0b803b99cf5f06d8ec", size = 5183081 }, - { url = "https://files.pythonhosted.org/packages/3d/23/c48607883f692a36c0a7735f47f98bad32dbe459a32d1568c0f21576985d/hf_xet-1.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d921ba32615676e436a0d15e162331abc9ed43d440916b1d836dc27ce1546173", size = 5356100 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/b2316c7f1076da0582b52ea228f68bea95e243c388440d1dc80297c9d813/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d9b03c34e13c44893ab6e8fea18ee8d2a6878c15328dd3aabedbdd83ee9f2ed3", size = 5647688 }, - { url = "https://files.pythonhosted.org/packages/2c/98/e6995f0fa579929da7795c961f403f4ee84af36c625963f52741d56f242c/hf_xet-1.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01b18608955b3d826307d37da8bd38b28a46cd2d9908b3a3655d1363274f941a", size = 5322627 }, - { url = "https://files.pythonhosted.org/packages/59/40/8f1d5a44a64d8bf9e3c19576e789f716af54875b46daae65426714e75db1/hf_xet-1.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:3562902c81299b09f3582ddfb324400c6a901a2f3bc854f83556495755f4954c", size = 2739542 }, + { url = "https://files.pythonhosted.org/packages/00/89/a1119eebe2836cb25758e7661d6410d3eae982e2b5e974bcc4d250be9012/hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23", size = 2687929, upload-time = "2025-06-20T21:48:32.284Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/2c78e28f309396e71ec8e4e9304a6483dcbc36172b5cea8f291994163425/hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8", size = 2556338, upload-time = "2025-06-20T21:48:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2f/6cad7b5fe86b7652579346cb7f85156c11761df26435651cbba89376cd2c/hf_xet-1.1.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc874b5c843e642f45fd85cda1ce599e123308ad2901ead23d3510a47ff506d1", size = 3102894, upload-time = "2025-06-20T21:48:28.114Z" }, + { url = "https://files.pythonhosted.org/packages/d0/54/0fcf2b619720a26fbb6cc941e89f2472a522cd963a776c089b189559447f/hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dbba1660e5d810bd0ea77c511a99e9242d920790d0e63c0e4673ed36c4022d18", size = 3002134, upload-time = "2025-06-20T21:48:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/1d351ac6cef7c4ba8c85744d37ffbfac2d53d0a6c04d2cabeba614640a78/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab34c4c3104133c495785d5d8bba3b1efc99de52c02e759cf711a91fd39d3a14", size = 3171009, upload-time = "2025-06-20T21:48:33.987Z" }, + { url = "https://files.pythonhosted.org/packages/c9/65/4b2ddb0e3e983f2508528eb4501288ae2f84963586fbdfae596836d5e57a/hf_xet-1.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:83088ecea236d5113de478acb2339f92c95b4fb0462acaa30621fac02f5a534a", size = 3279245, upload-time = "2025-06-20T21:48:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/f0/55/ef77a85ee443ae05a9e9cba1c9f0dd9241eb42da2aeba1dc50f51154c81a/hf_xet-1.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:73e167d9807d166596b4b2f0b585c6d5bd84a26dea32843665a8b58f6edba245", size = 2738931, upload-time = "2025-06-20T21:48:39.482Z" }, ] [[package]] name = "hiredis" version = "3.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/08/24b72f425b75e1de7442fb1740f69ca66d5820b9f9c0e2511ff9aadab3b7/hiredis-3.2.1.tar.gz", hash = "sha256:5a5f64479bf04dd829fe7029fad0ea043eac4023abc6e946668cbbec3493a78d", size = 89096 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/84/2ea9636f2ba0811d9eb3bebbbfa84f488238180ddab70c9cb7fa13419d78/hiredis-3.2.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4ae0be44cab5e74e6e4c4a93d04784629a45e781ff483b136cc9e1b9c23975c", size = 82425 }, - { url = "https://files.pythonhosted.org/packages/fc/24/b9ebf766a99998fda3975937afa4912e98de9d7f8d0b83f48096bdd961c1/hiredis-3.2.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:24647e84c9f552934eb60b7f3d2116f8b64a7020361da9369e558935ca45914d", size = 45231 }, - { url = "https://files.pythonhosted.org/packages/68/4c/c009b4d9abeb964d607f0987561892d1589907f770b9e5617552b34a4a4d/hiredis-3.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fb3e92d1172da8decc5f836bf8b528c0fc9b6d449f1353e79ceeb9dc1801132", size = 43240 }, - { url = "https://files.pythonhosted.org/packages/e9/83/d53f3ae9e4ac51b8a35afb7ccd68db871396ed1d7c8ba02ce2c30de0cf17/hiredis-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38ba7a32e51e518b6b3e470142e52ed2674558e04d7d73d86eb19ebcb37d7d40", size = 169624 }, - { url = "https://files.pythonhosted.org/packages/91/2f/f9f091526e22a45385d45f3870204dc78aee365b6fe32e679e65674da6a7/hiredis-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fc632be73174891d6bb71480247e57b2fd8f572059f0a1153e4d0339e919779", size = 165799 }, - { url = "https://files.pythonhosted.org/packages/1c/cc/e561274438cdb19794f0638136a5a99a9ca19affcb42679b12a78016b8ad/hiredis-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f03e6839ff21379ad3c195e0700fc9c209e7f344946dea0f8a6d7b5137a2a141", size = 180612 }, - { url = "https://files.pythonhosted.org/packages/83/ba/a8a989f465191d55672e57aea2a331bfa3a74b5cbc6f590031c9e11f7491/hiredis-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99983873e37c71bb71deb544670ff4f9d6920dab272aaf52365606d87a4d6c73", size = 169934 }, - { url = "https://files.pythonhosted.org/packages/52/5f/1148e965df1c67b17bdcaef199f54aec3def0955d19660a39c6ee10a6f55/hiredis-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd982c419f48e3a57f592678c72474429465bb4bfc96472ec805f5d836523f0", size = 170074 }, - { url = "https://files.pythonhosted.org/packages/43/5e/e6846ad159a938b539fb8d472e2e68cb6758d7c9454ea0520211f335ea72/hiredis-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc993f4aa4abc029347f309e722f122e05a3b8a0c279ae612849b5cc9dc69f2d", size = 164158 }, - { url = "https://files.pythonhosted.org/packages/0a/a1/5891e0615f0993f194c1b51a65aaac063b0db318a70df001b28e49f0579d/hiredis-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dde790d420081f18b5949227649ccb3ed991459df33279419a25fcae7f97cd92", size = 162591 }, - { url = "https://files.pythonhosted.org/packages/d4/da/8bce52ca81716f53c1014f689aea4c170ba6411e6848f81a1bed1fc375eb/hiredis-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0c8cae7edbef860afcf3177b705aef43e10b5628f14d5baf0ec69668247d08d", size = 174808 }, - { url = "https://files.pythonhosted.org/packages/84/91/fc1ef444ed4dc432b5da9b48e9bd23266c703528db7be19e2b608d67ba06/hiredis-3.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e8a90eaca7e1ce7f175584f07a2cdbbcab13f4863f9f355d7895c4d28805f65b", size = 167060 }, - { url = "https://files.pythonhosted.org/packages/66/ad/beebf73a5455f232b97e00564d1e8ad095d4c6e18858c60c6cfdd893ac1e/hiredis-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:476031958fa44e245e803827e0787d49740daa4de708fe514370293ce519893a", size = 164833 }, - { url = "https://files.pythonhosted.org/packages/75/79/a9591bdc0148c0fbdf54cf6f3d449932d3b3b8779e87f33fa100a5a8088f/hiredis-3.2.1-cp311-cp311-win32.whl", hash = "sha256:eb3f5df2a9593b4b4b676dce3cea53b9c6969fc372875188589ddf2bafc7f624", size = 20402 }, - { url = "https://files.pythonhosted.org/packages/9f/05/c93cc6fab31e3c01b671126c82f44372fb211facb8bd4571fd372f50898d/hiredis-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1402e763d8a9fdfcc103bbf8b2913971c0a3f7b8a73deacbda3dfe5f3a9d1e0b", size = 22085 }, - { url = "https://files.pythonhosted.org/packages/60/a1/6da1578a22df1926497f7a3f6a3d2408fe1d1559f762c1640af5762a8eb6/hiredis-3.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3742d8b17e73c198cabeab11da35f2e2a81999d406f52c6275234592256bf8e8", size = 82627 }, - { url = "https://files.pythonhosted.org/packages/6c/b1/1056558ca8dc330be5bb25162fe5f268fee71571c9a535153df9f871a073/hiredis-3.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c2f3176fb617a79f6cccf22cb7d2715e590acb534af6a82b41f8196ad59375d", size = 45404 }, - { url = "https://files.pythonhosted.org/packages/58/4f/13d1fa1a6b02a99e9fed8f546396f2d598c3613c98e6c399a3284fa65361/hiredis-3.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8bd46189c7fa46174e02670dc44dfecb60f5bd4b67ed88cb050d8f1fd842f09", size = 43299 }, - { url = "https://files.pythonhosted.org/packages/c0/25/ddfac123ba5a32eb1f0b40ba1b2ec98a599287f7439def8856c3c7e5dd0d/hiredis-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86ee4488c8575b58139cdfdddeae17f91e9a893ffee20260822add443592e2f", size = 172194 }, - { url = "https://files.pythonhosted.org/packages/2c/1e/443a3703ce570b631ca43494094fbaeb051578a0ebe4bfcefde351e1ba25/hiredis-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3717832f4a557b2fe7060b9d4a7900e5de287a15595e398c3f04df69019ca69d", size = 168429 }, - { url = "https://files.pythonhosted.org/packages/3b/d6/0d8c6c706ed79b2298c001b5458c055615e3166533dcee3900e821a18a3e/hiredis-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5cb12c21fb9e2403d28c4e6a38120164973342d34d08120f2d7009b66785644", size = 182967 }, - { url = "https://files.pythonhosted.org/packages/da/68/da8dd231fbce858b5a20ab7d7bf558912cd125f08bac4c778865ef5fe2c2/hiredis-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:080fda1510bbd389af91f919c11a4f2aa4d92f0684afa4709236faa084a42cac", size = 172495 }, - { url = "https://files.pythonhosted.org/packages/65/25/83a31420535e2778662caa95533d5c997011fa6a88331f0cdb22afea9ec3/hiredis-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1252e10a1f3273d1c6bf2021e461652c2e11b05b83e0915d6eb540ec7539afe2", size = 173142 }, - { url = "https://files.pythonhosted.org/packages/41/d7/cb907348889eb75e2aa2e6b63e065b611459e0f21fe1e371a968e13f0d55/hiredis-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9e320e99ab7d2a30dc91ff6f745ba38d39b23f43d345cdee9881329d7b511d6", size = 166433 }, - { url = "https://files.pythonhosted.org/packages/01/5d/7cbc69d82af7b29a95723d50f5261555ba3d024bfbdc414bdc3d23c0defb/hiredis-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:641668f385f16550fdd6fdc109b0af6988b94ba2acc06770a5e06a16e88f320c", size = 164883 }, - { url = "https://files.pythonhosted.org/packages/f9/00/f995b1296b1d7e0247651347aa230f3225a9800e504fdf553cf7cd001cf7/hiredis-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e1f44208c39d6c345ff451f82f21e9eeda6fe9af4ac65972cc3eeb58d41f7cb", size = 177262 }, - { url = "https://files.pythonhosted.org/packages/c5/f3/723a67d729e94764ce9e0d73fa5f72a0f87d3ce3c98c9a0b27cbf001cc79/hiredis-3.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f882a0d6415fffe1ffcb09e6281d0ba8b1ece470e866612bbb24425bf76cf397", size = 169619 }, - { url = "https://files.pythonhosted.org/packages/45/58/f69028df00fb1b223e221403f3be2059ae86031e7885f955d26236bdfc17/hiredis-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4e78719a0730ebffe335528531d154bc8867a246418f74ecd88adbc4d938c49", size = 167303 }, - { url = "https://files.pythonhosted.org/packages/2b/7d/567411e65cce76cf265a9a4f837fd2ebc564bef6368dd42ac03f7a517c0a/hiredis-3.2.1-cp312-cp312-win32.whl", hash = "sha256:33c4604d9f79a13b84da79950a8255433fca7edaf292bbd3364fd620864ed7b2", size = 20551 }, - { url = "https://files.pythonhosted.org/packages/90/74/b4c291eb4a4a874b3690ff9fc311a65d5292072556421b11b1d786e3e1d0/hiredis-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b9749375bf9d171aab8813694f379f2cff0330d7424000f5e92890ad4932dc9", size = 22128 }, +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/24b72f425b75e1de7442fb1740f69ca66d5820b9f9c0e2511ff9aadab3b7/hiredis-3.2.1.tar.gz", hash = "sha256:5a5f64479bf04dd829fe7029fad0ea043eac4023abc6e946668cbbec3493a78d", size = 89096, upload-time = "2025-05-23T11:41:57.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/84/2ea9636f2ba0811d9eb3bebbbfa84f488238180ddab70c9cb7fa13419d78/hiredis-3.2.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:e4ae0be44cab5e74e6e4c4a93d04784629a45e781ff483b136cc9e1b9c23975c", size = 82425, upload-time = "2025-05-23T11:39:54.135Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/b9ebf766a99998fda3975937afa4912e98de9d7f8d0b83f48096bdd961c1/hiredis-3.2.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:24647e84c9f552934eb60b7f3d2116f8b64a7020361da9369e558935ca45914d", size = 45231, upload-time = "2025-05-23T11:39:55.455Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c009b4d9abeb964d607f0987561892d1589907f770b9e5617552b34a4a4d/hiredis-3.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fb3e92d1172da8decc5f836bf8b528c0fc9b6d449f1353e79ceeb9dc1801132", size = 43240, upload-time = "2025-05-23T11:39:57.8Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/d53f3ae9e4ac51b8a35afb7ccd68db871396ed1d7c8ba02ce2c30de0cf17/hiredis-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38ba7a32e51e518b6b3e470142e52ed2674558e04d7d73d86eb19ebcb37d7d40", size = 169624, upload-time = "2025-05-23T11:40:00.055Z" }, + { url = "https://files.pythonhosted.org/packages/91/2f/f9f091526e22a45385d45f3870204dc78aee365b6fe32e679e65674da6a7/hiredis-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4fc632be73174891d6bb71480247e57b2fd8f572059f0a1153e4d0339e919779", size = 165799, upload-time = "2025-05-23T11:40:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cc/e561274438cdb19794f0638136a5a99a9ca19affcb42679b12a78016b8ad/hiredis-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f03e6839ff21379ad3c195e0700fc9c209e7f344946dea0f8a6d7b5137a2a141", size = 180612, upload-time = "2025-05-23T11:40:02.385Z" }, + { url = "https://files.pythonhosted.org/packages/83/ba/a8a989f465191d55672e57aea2a331bfa3a74b5cbc6f590031c9e11f7491/hiredis-3.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99983873e37c71bb71deb544670ff4f9d6920dab272aaf52365606d87a4d6c73", size = 169934, upload-time = "2025-05-23T11:40:03.524Z" }, + { url = "https://files.pythonhosted.org/packages/52/5f/1148e965df1c67b17bdcaef199f54aec3def0955d19660a39c6ee10a6f55/hiredis-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffd982c419f48e3a57f592678c72474429465bb4bfc96472ec805f5d836523f0", size = 170074, upload-time = "2025-05-23T11:40:04.618Z" }, + { url = "https://files.pythonhosted.org/packages/43/5e/e6846ad159a938b539fb8d472e2e68cb6758d7c9454ea0520211f335ea72/hiredis-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc993f4aa4abc029347f309e722f122e05a3b8a0c279ae612849b5cc9dc69f2d", size = 164158, upload-time = "2025-05-23T11:40:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a1/5891e0615f0993f194c1b51a65aaac063b0db318a70df001b28e49f0579d/hiredis-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dde790d420081f18b5949227649ccb3ed991459df33279419a25fcae7f97cd92", size = 162591, upload-time = "2025-05-23T11:40:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/d4/da/8bce52ca81716f53c1014f689aea4c170ba6411e6848f81a1bed1fc375eb/hiredis-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b0c8cae7edbef860afcf3177b705aef43e10b5628f14d5baf0ec69668247d08d", size = 174808, upload-time = "2025-05-23T11:40:09.146Z" }, + { url = "https://files.pythonhosted.org/packages/84/91/fc1ef444ed4dc432b5da9b48e9bd23266c703528db7be19e2b608d67ba06/hiredis-3.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e8a90eaca7e1ce7f175584f07a2cdbbcab13f4863f9f355d7895c4d28805f65b", size = 167060, upload-time = "2025-05-23T11:40:10.757Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/beebf73a5455f232b97e00564d1e8ad095d4c6e18858c60c6cfdd893ac1e/hiredis-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:476031958fa44e245e803827e0787d49740daa4de708fe514370293ce519893a", size = 164833, upload-time = "2025-05-23T11:40:12.001Z" }, + { url = "https://files.pythonhosted.org/packages/75/79/a9591bdc0148c0fbdf54cf6f3d449932d3b3b8779e87f33fa100a5a8088f/hiredis-3.2.1-cp311-cp311-win32.whl", hash = "sha256:eb3f5df2a9593b4b4b676dce3cea53b9c6969fc372875188589ddf2bafc7f624", size = 20402, upload-time = "2025-05-23T11:40:13.216Z" }, + { url = "https://files.pythonhosted.org/packages/9f/05/c93cc6fab31e3c01b671126c82f44372fb211facb8bd4571fd372f50898d/hiredis-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1402e763d8a9fdfcc103bbf8b2913971c0a3f7b8a73deacbda3dfe5f3a9d1e0b", size = 22085, upload-time = "2025-05-23T11:40:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/60/a1/6da1578a22df1926497f7a3f6a3d2408fe1d1559f762c1640af5762a8eb6/hiredis-3.2.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3742d8b17e73c198cabeab11da35f2e2a81999d406f52c6275234592256bf8e8", size = 82627, upload-time = "2025-05-23T11:40:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b1/1056558ca8dc330be5bb25162fe5f268fee71571c9a535153df9f871a073/hiredis-3.2.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c2f3176fb617a79f6cccf22cb7d2715e590acb534af6a82b41f8196ad59375d", size = 45404, upload-time = "2025-05-23T11:40:16.72Z" }, + { url = "https://files.pythonhosted.org/packages/58/4f/13d1fa1a6b02a99e9fed8f546396f2d598c3613c98e6c399a3284fa65361/hiredis-3.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a8bd46189c7fa46174e02670dc44dfecb60f5bd4b67ed88cb050d8f1fd842f09", size = 43299, upload-time = "2025-05-23T11:40:17.697Z" }, + { url = "https://files.pythonhosted.org/packages/c0/25/ddfac123ba5a32eb1f0b40ba1b2ec98a599287f7439def8856c3c7e5dd0d/hiredis-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f86ee4488c8575b58139cdfdddeae17f91e9a893ffee20260822add443592e2f", size = 172194, upload-time = "2025-05-23T11:40:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/443a3703ce570b631ca43494094fbaeb051578a0ebe4bfcefde351e1ba25/hiredis-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3717832f4a557b2fe7060b9d4a7900e5de287a15595e398c3f04df69019ca69d", size = 168429, upload-time = "2025-05-23T11:40:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/0d8c6c706ed79b2298c001b5458c055615e3166533dcee3900e821a18a3e/hiredis-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5cb12c21fb9e2403d28c4e6a38120164973342d34d08120f2d7009b66785644", size = 182967, upload-time = "2025-05-23T11:40:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/da8dd231fbce858b5a20ab7d7bf558912cd125f08bac4c778865ef5fe2c2/hiredis-3.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:080fda1510bbd389af91f919c11a4f2aa4d92f0684afa4709236faa084a42cac", size = 172495, upload-time = "2025-05-23T11:40:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/65/25/83a31420535e2778662caa95533d5c997011fa6a88331f0cdb22afea9ec3/hiredis-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1252e10a1f3273d1c6bf2021e461652c2e11b05b83e0915d6eb540ec7539afe2", size = 173142, upload-time = "2025-05-23T11:40:24.24Z" }, + { url = "https://files.pythonhosted.org/packages/41/d7/cb907348889eb75e2aa2e6b63e065b611459e0f21fe1e371a968e13f0d55/hiredis-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d9e320e99ab7d2a30dc91ff6f745ba38d39b23f43d345cdee9881329d7b511d6", size = 166433, upload-time = "2025-05-23T11:40:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/7cbc69d82af7b29a95723d50f5261555ba3d024bfbdc414bdc3d23c0defb/hiredis-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:641668f385f16550fdd6fdc109b0af6988b94ba2acc06770a5e06a16e88f320c", size = 164883, upload-time = "2025-05-23T11:40:26.454Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/f995b1296b1d7e0247651347aa230f3225a9800e504fdf553cf7cd001cf7/hiredis-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e1f44208c39d6c345ff451f82f21e9eeda6fe9af4ac65972cc3eeb58d41f7cb", size = 177262, upload-time = "2025-05-23T11:40:27.576Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/723a67d729e94764ce9e0d73fa5f72a0f87d3ce3c98c9a0b27cbf001cc79/hiredis-3.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f882a0d6415fffe1ffcb09e6281d0ba8b1ece470e866612bbb24425bf76cf397", size = 169619, upload-time = "2025-05-23T11:40:29.671Z" }, + { url = "https://files.pythonhosted.org/packages/45/58/f69028df00fb1b223e221403f3be2059ae86031e7885f955d26236bdfc17/hiredis-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4e78719a0730ebffe335528531d154bc8867a246418f74ecd88adbc4d938c49", size = 167303, upload-time = "2025-05-23T11:40:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7d/567411e65cce76cf265a9a4f837fd2ebc564bef6368dd42ac03f7a517c0a/hiredis-3.2.1-cp312-cp312-win32.whl", hash = "sha256:33c4604d9f79a13b84da79950a8255433fca7edaf292bbd3364fd620864ed7b2", size = 20551, upload-time = "2025-05-23T11:40:32.69Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/b4c291eb4a4a874b3690ff9fc311a65d5292072556421b11b1d786e3e1d0/hiredis-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b9749375bf9d171aab8813694f379f2cff0330d7424000f5e92890ad4932dc9", size = 22128, upload-time = "2025-05-23T11:40:33.686Z" }, ] [[package]] name = "hpack" version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, ] [[package]] @@ -2430,9 +2461,9 @@ dependencies = [ { name = "six" }, { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 }, + { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, ] [[package]] @@ -2443,9 +2474,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] @@ -2455,31 +2486,31 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116, upload-time = "2023-03-21T22:29:37.214Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 }, + { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854, upload-time = "2023-03-21T22:29:35.683Z" }, ] [[package]] name = "httptools" version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, ] [[package]] @@ -2493,9 +2524,9 @@ dependencies = [ { name = "idna" }, { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, ] [package.optional-dependencies] @@ -2506,9 +2537,18 @@ socks = [ { name = "socksio" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + [[package]] name = "huggingface-hub" -version = "0.32.3" +version = "0.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -2520,9 +2560,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/74/c4961b31e0f142a032ea24f477c3a7524dfabfd8126398a968b3cc6bf804/huggingface_hub-0.32.3.tar.gz", hash = "sha256:752c889ebf3a63cbd39803f6d87ccc135a463bbcb36abfa2faff0ccbf1cec087", size = 424525 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/dc/4f4d8080cbce7a38c1d0f1ba4932f9134480b9761af8ef4c65d49254b2bd/huggingface_hub-0.32.3-py3-none-any.whl", hash = "sha256:e46f7ea7fe2b5e5f67cc4e37eb201140091946a314d7c2b134a9673dadd80b6a", size = 512094 }, + { url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" }, ] [[package]] @@ -2532,27 +2572,40 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] [[package]] name = "hyperframe" version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.135.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/83/15c4e30561a0d8c8d076c88cb159187823d877118f34c851ada3b9b02a7b/hypothesis-6.135.26.tar.gz", hash = "sha256:73af0e46cd5039c6806f514fed6a3c185d91ef88b5a1577477099ddbd1a2e300", size = 454523, upload-time = "2025-07-05T04:59:45.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, + { url = "https://files.pythonhosted.org/packages/3c/78/db4fdc464219455f8dde90074660c3faf8429101b2d1299cac7d219e3176/hypothesis-6.135.26-py3-none-any.whl", hash = "sha256:fa237cbe2ae2c31d65f7230dcb866139ace635dcfec6c30dddf25974dd8ff4b9", size = 521517, upload-time = "2025-07-05T04:59:42.061Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] @@ -2562,52 +2615,52 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320, upload-time = "2024-08-20T17:11:42.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269 }, + { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269, upload-time = "2024-08-20T17:11:41.102Z" }, ] [[package]] name = "importlib-resources" version = "6.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jieba" version = "0.42.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } [[package]] name = "jinja2" @@ -2616,68 +2669,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jiter" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, - { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, - { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, - { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, - { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, - { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, - { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, - { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, - { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, - { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, - { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, - { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, - { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 }, - { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 }, - { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 }, - { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 }, - { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 }, - { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 }, - { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 }, - { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 }, - { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 }, - { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 }, - { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 }, - { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473, upload-time = "2025-05-18T19:03:25.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869, upload-time = "2025-05-18T19:03:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765, upload-time = "2025-05-18T19:03:41.271Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234, upload-time = "2025-05-18T19:03:42.918Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" }, ] [[package]] name = "jmespath" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607 } +sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489 }, + { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, ] [[package]] name = "json-repair" -version = "0.46.0" +version = "0.47.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/5a/5c14f14735438eb27fd4eb9bb4eb273c996c0d1d959182382cbc618f99dd/json_repair-0.46.0.tar.gz", hash = "sha256:abc751162baf8e384685558acba978478e833c1207be31468d9babfaf8029ab6", size = 33321 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9e/e8bcda4fd47b16fcd4f545af258d56ba337fa43b847beb213818d7641515/json_repair-0.47.6.tar.gz", hash = "sha256:4af5a14b9291d4d005a11537bae5a6b7912376d7584795f0ac1b23724b999620", size = 34400, upload-time = "2025-07-01T15:42:07.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/ea/8ca71883f10acf4449720b86dd85cea51d7a71bec2558d5275ae0dcc66e4/json_repair-0.46.0-py3-none-any.whl", hash = "sha256:54d6a9889fba0846b80befb2b1aca619103ad3ed74612fb3fedd965a4a3b1653", size = 22255 }, + { url = "https://files.pythonhosted.org/packages/bb/f8/f464ce2afc4be5decf53d0171c2d399d9ee6cd70d2273b8e85e7c6d00324/json_repair-0.47.6-py3-none-any.whl", hash = "sha256:1c9da58fb6240f99b8405f63534e08f8402793f09074dea25800a0b232d4fb19", size = 25754, upload-time = "2025-07-01T15:42:06.418Z" }, ] [[package]] @@ -2690,9 +2743,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 }, + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, ] [[package]] @@ -2702,9 +2755,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] @@ -2717,14 +2770,14 @@ dependencies = [ { name = "tzdata" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034 }, + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, ] [[package]] name = "kubernetes" -version = "32.0.1" +version = "33.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2739,9 +2792,9 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/0598f0e8b4af37cd9b10d8b87386cf3173cb8045d834ab5f6ec347a758b3/kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28", size = 946691 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/10/9f8af3e6f569685ce3af7faab51c8dd9d93b9c38eba339ca31c746119447/kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998", size = 1988070 }, + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, ] [[package]] @@ -2751,7 +2804,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } [[package]] name = "langfuse" @@ -2766,9 +2819,9 @@ dependencies = [ { name = "pydantic" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/e9/22c9c05d877ab85da6d9008aaa7360f2a9ad58787a8e36e00b1b5be9a990/langfuse-2.51.5.tar.gz", hash = "sha256:55bc37b5c5d3ae133c1a95db09117cfb3117add110ba02ebbf2ce45ac4395c5b", size = 117574 } +sdist = { url = "https://files.pythonhosted.org/packages/3c/e9/22c9c05d877ab85da6d9008aaa7360f2a9ad58787a8e36e00b1b5be9a990/langfuse-2.51.5.tar.gz", hash = "sha256:55bc37b5c5d3ae133c1a95db09117cfb3117add110ba02ebbf2ce45ac4395c5b", size = 117574, upload-time = "2024-10-09T00:59:15.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/f7/242a13ca094c78464b7d4df77dfe7d4c44ed77b15fed3d2e3486afa5d2e1/langfuse-2.51.5-py3-none-any.whl", hash = "sha256:b95401ca710ef94b521afa6541933b6f93d7cfd4a97523c8fc75bca4d6d219fb", size = 214281 }, + { url = "https://files.pythonhosted.org/packages/03/f7/242a13ca094c78464b7d4df77dfe7d4c44ed77b15fed3d2e3486afa5d2e1/langfuse-2.51.5-py3-none-any.whl", hash = "sha256:b95401ca710ef94b521afa6541933b6f93d7cfd4a97523c8fc75bca4d6d219fb", size = 214281, upload-time = "2024-10-09T00:59:12.596Z" }, ] [[package]] @@ -2782,56 +2835,9 @@ dependencies = [ { name = "requests" }, { name = "requests-toolbelt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/56/201dd94d492ae47c1bf9b50cacc1985113dc2288d8f15857e1f4a6818376/langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a", size = 300453 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/56/201dd94d492ae47c1bf9b50cacc1985113dc2288d8f15857e1f4a6818376/langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a", size = 300453, upload-time = "2024-11-27T17:32:41.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812 }, -] - -[[package]] -name = "levenshtein" -version = "0.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rapidfuzz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/b3/b5f8011483ba9083a0bc74c4d58705e9cf465fbe55c948a1b1357d0a2aa8/levenshtein-0.27.1.tar.gz", hash = "sha256:3e18b73564cfc846eec94dd13fab6cb006b5d2e0cc56bad1fd7d5585881302e3", size = 382571 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/84/110136e740655779aceb0da2399977362f21b2dbf3ea3646557f9c2237c4/levenshtein-0.27.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6f1760108319a108dceb2f02bc7cdb78807ad1f9c673c95eaa1d0fe5dfcaae", size = 174555 }, - { url = "https://files.pythonhosted.org/packages/19/5b/176d96959f5c5969f356d8856f8e20d2e72f7e4879f6d1cda8e5c2ac2614/levenshtein-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c4ed8400d94ab348099395e050b8ed9dd6a5d6b5b9e75e78b2b3d0b5f5b10f38", size = 156286 }, - { url = "https://files.pythonhosted.org/packages/2a/2d/a75abaafc8a46b0dc52ab14dc96708989a31799a02a4914f9210c3415f04/levenshtein-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7826efe51be8ff58bc44a633e022fdd4b9fc07396375a6dbc4945a3bffc7bf8f", size = 152413 }, - { url = "https://files.pythonhosted.org/packages/9a/5f/533f4adf964b10817a1d0ecca978b3542b3b9915c96172d20162afe18bed/levenshtein-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff5afb78719659d353055863c7cb31599fbea6865c0890b2d840ee40214b3ddb", size = 184236 }, - { url = "https://files.pythonhosted.org/packages/02/79/e698623795e36e0d166a3aa1eac6fe1e446cac3a5c456664a95c351571d1/levenshtein-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:201dafd5c004cd52018560cf3213da799534d130cf0e4db839b51f3f06771de0", size = 185502 }, - { url = "https://files.pythonhosted.org/packages/ac/94/76b64762f4af6e20bbab79713c4c48783240e6e502b2f52e5037ddda688a/levenshtein-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ddd59f3cfaec216811ee67544779d9e2d6ed33f79337492a248245d6379e3d", size = 161749 }, - { url = "https://files.pythonhosted.org/packages/56/d0/d10eff9224c94a478078a469aaeb43471fdeddad035f443091224c7544b8/levenshtein-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6afc241d27ecf5b921063b796812c55b0115423ca6fa4827aa4b1581643d0a65", size = 246686 }, - { url = "https://files.pythonhosted.org/packages/b2/8a/ebbeff74461da3230d00e8a8197480a2ea1a9bbb7dbc273214d7ea3896cb/levenshtein-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee2e766277cceb8ca9e584ea03b8dc064449ba588d3e24c1923e4b07576db574", size = 1116616 }, - { url = "https://files.pythonhosted.org/packages/1d/9b/e7323684f833ede13113fba818c3afe665a78b47d720afdeb2e530c1ecb3/levenshtein-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:920b23d6109453913ce78ec451bc402ff19d020ee8be4722e9d11192ec2fac6f", size = 1401483 }, - { url = "https://files.pythonhosted.org/packages/ef/1d/9b6ab30ff086a33492d6f7de86a07050b15862ccf0d9feeccfbe26af52d8/levenshtein-0.27.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:560d7edba126e2eea3ac3f2f12e7bd8bc9c6904089d12b5b23b6dfa98810b209", size = 1225805 }, - { url = "https://files.pythonhosted.org/packages/1b/07/ae2f31e87ff65ba4857e25192646f1f3c8cca83c2ac1c27e551215b7e1b6/levenshtein-0.27.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8d5362b6c7aa4896dc0cb1e7470a4ad3c06124e0af055dda30d81d3c5549346b", size = 1419860 }, - { url = "https://files.pythonhosted.org/packages/43/d2/dfcc5c22c07bab9be99f3f47a907be583bcd37bfd2eec57a205e59671019/levenshtein-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:65ba880815b0f80a80a293aeebac0fab8069d03ad2d6f967a886063458f9d7a1", size = 1188823 }, - { url = "https://files.pythonhosted.org/packages/8b/96/713335623f8ab50eba0627c8685618dc3a985aedaaea9f492986b9443551/levenshtein-0.27.1-cp311-cp311-win32.whl", hash = "sha256:fcc08effe77fec0bc5b0f6f10ff20b9802b961c4a69047b5499f383119ddbe24", size = 88156 }, - { url = "https://files.pythonhosted.org/packages/aa/ae/444d6e8ba9a35379a56926716f18bb2e77c6cf69e5324521fbe6885f14f6/levenshtein-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:0ed402d8902be7df212ac598fc189f9b2d520817fdbc6a05e2ce44f7f3ef6857", size = 100399 }, - { url = "https://files.pythonhosted.org/packages/80/c0/ff226897a238a2deb2ca2c00d658755a1aa01884b0ddc8f5d406cb5f2b0d/levenshtein-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:7fdaab29af81a8eb981043737f42450efca64b9761ca29385487b29c506da5b5", size = 88033 }, - { url = "https://files.pythonhosted.org/packages/0d/73/84a7126b9e6441c2547f1fbfd65f3c15c387d1fc04e0dd1d025a12107771/levenshtein-0.27.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25fb540d8c55d1dc7bdc59b7de518ea5ed9df92eb2077e74bcb9bb6de7b06f69", size = 173953 }, - { url = "https://files.pythonhosted.org/packages/8f/5c/06c01870c0cf336f9f29397bbfbfbbfd3a59918868716e7bb15828e89367/levenshtein-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f09cfab6387e9c908c7b37961c045e8e10eb9b7ec4a700367f8e080ee803a562", size = 156399 }, - { url = "https://files.pythonhosted.org/packages/c7/4a/c1d3f27ec8b3fff5a96617251bf3f61c67972869ac0a0419558fc3e2cbe6/levenshtein-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafa29c0e616f322b574e0b2aeb5b1ff2f8d9a1a6550f22321f3bd9bb81036e3", size = 151061 }, - { url = "https://files.pythonhosted.org/packages/4d/8f/2521081e9a265891edf46aa30e1b59c1f347a452aed4c33baafbec5216fa/levenshtein-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be7a7642ea64392fa1e6ef7968c2e50ef2152c60948f95d0793361ed97cf8a6f", size = 183119 }, - { url = "https://files.pythonhosted.org/packages/1f/a0/a63e3bce6376127596d04be7f57e672d2f3d5f540265b1e30b9dd9b3c5a9/levenshtein-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:060b48c45ed54bcea9582ce79c6365b20a1a7473767e0b3d6be712fa3a22929c", size = 185352 }, - { url = "https://files.pythonhosted.org/packages/17/8c/8352e992063952b38fb61d49bad8d193a4a713e7eeceb3ae74b719d7863d/levenshtein-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:712f562c5e64dd0398d3570fe99f8fbb88acec7cc431f101cb66c9d22d74c542", size = 159879 }, - { url = "https://files.pythonhosted.org/packages/69/b4/564866e2038acf47c3de3e9292fc7fc7cc18d2593fedb04f001c22ac6e15/levenshtein-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6141ad65cab49aa4527a3342d76c30c48adb2393b6cdfeca65caae8d25cb4b8", size = 245005 }, - { url = "https://files.pythonhosted.org/packages/ba/f9/7367f87e3a6eed282f3654ec61a174b4d1b78a7a73f2cecb91f0ab675153/levenshtein-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:799b8d73cda3265331116f62932f553804eae16c706ceb35aaf16fc2a704791b", size = 1116865 }, - { url = "https://files.pythonhosted.org/packages/f5/02/b5b3bfb4b4cd430e9d110bad2466200d51c6061dae7c5a64e36047c8c831/levenshtein-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ec99871d98e517e1cc4a15659c62d6ea63ee5a2d72c5ddbebd7bae8b9e2670c8", size = 1401723 }, - { url = "https://files.pythonhosted.org/packages/ef/69/b93bccd093b3f06a99e67e11ebd6e100324735dc2834958ba5852a1b9fed/levenshtein-0.27.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8799164e1f83588dbdde07f728ea80796ea72196ea23484d78d891470241b222", size = 1226276 }, - { url = "https://files.pythonhosted.org/packages/ab/32/37dd1bc5ce866c136716619e6f7081d7078d7dd1c1da7025603dcfd9cf5f/levenshtein-0.27.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:583943813898326516ab451a83f734c6f07488cda5c361676150d3e3e8b47927", size = 1420132 }, - { url = "https://files.pythonhosted.org/packages/4b/08/f3bc828dd9f0f8433b26f37c4fceab303186ad7b9b70819f2ccb493d99fc/levenshtein-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bb22956af44bb4eade93546bf95be610c8939b9a9d4d28b2dfa94abf454fed7", size = 1189144 }, - { url = "https://files.pythonhosted.org/packages/2d/54/5ecd89066cf579223d504abe3ac37ba11f63b01a19fd12591083acc00eb6/levenshtein-0.27.1-cp312-cp312-win32.whl", hash = "sha256:d9099ed1bcfa7ccc5540e8ad27b5dc6f23d16addcbe21fdd82af6440f4ed2b6d", size = 88279 }, - { url = "https://files.pythonhosted.org/packages/53/79/4f8fabcc5aca9305b494d1d6c7a98482e90a855e0050ae9ff5d7bf4ab2c6/levenshtein-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:7f071ecdb50aa6c15fd8ae5bcb67e9da46ba1df7bba7c6bf6803a54c7a41fd96", size = 100659 }, - { url = "https://files.pythonhosted.org/packages/cb/81/f8e4c0f571c2aac2e0c56a6e0e41b679937a2b7013e79415e4aef555cff0/levenshtein-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:83b9033a984ccace7703f35b688f3907d55490182fd39b33a8e434d7b2e249e6", size = 88168 }, - { url = "https://files.pythonhosted.org/packages/7d/44/c5955d0b6830925559b00617d80c9f6e03a9b00c451835ee4da7010e71cd/levenshtein-0.27.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:909b7b6bce27a4ec90576c9a9bd9af5a41308dfecf364b410e80b58038277bbe", size = 170533 }, - { url = "https://files.pythonhosted.org/packages/e7/3f/858572d68b33e13a9c154b99f153317efe68381bf63cc4e986e820935fc3/levenshtein-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d193a7f97b8c6a350e36ec58e41a627c06fa4157c3ce4b2b11d90cfc3c2ebb8f", size = 153119 }, - { url = "https://files.pythonhosted.org/packages/d1/60/2bd8d001ea4eb53ca16faa7a649d56005ba22b1bcc2a4f1617ab27ed7e48/levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:614be316e3c06118705fae1f717f9072d35108e5fd4e66a7dd0e80356135340b", size = 149576 }, - { url = "https://files.pythonhosted.org/packages/e4/db/0580797e1e4ac26cf67761a235b29b49f62d2b175dbbc609882f2aecd4e4/levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31fc0a5bb070722bdabb6f7e14955a294a4a968c68202d294699817f21545d22", size = 157445 }, - { url = "https://files.pythonhosted.org/packages/f4/de/9c171c96d1f15c900086d7212b5543a85539e767689fc4933d14048ba1ec/levenshtein-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9415aa5257227af543be65768a80c7a75e266c3c818468ce6914812f88f9c3df", size = 243141 }, - { url = "https://files.pythonhosted.org/packages/dc/1e/408fd10217eac0e43aea0604be22b4851a09e03d761d44d4ea12089dd70e/levenshtein-0.27.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7987ef006a3cf56a4532bd4c90c2d3b7b4ca9ad3bf8ae1ee5713c4a3bdfda913", size = 98045 }, + { url = "https://files.pythonhosted.org/packages/de/f0/63b06b99b730b9954f8709f6f7d9b8d076fa0a973e472efe278089bde42b/langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15", size = 311812, upload-time = "2024-11-27T17:32:39.569Z" }, ] [[package]] @@ -2851,102 +2857,98 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/7a/6c1994a239abd1b335001a46ae47fa055a24c493b6de19a9fa1872187fe9/litellm-1.63.7.tar.gz", hash = "sha256:2fbd7236d5e5379eee18556857ed62a5ed49f4f09e03ff33cf15932306b984f1", size = 6598034 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/7a/6c1994a239abd1b335001a46ae47fa055a24c493b6de19a9fa1872187fe9/litellm-1.63.7.tar.gz", hash = "sha256:2fbd7236d5e5379eee18556857ed62a5ed49f4f09e03ff33cf15932306b984f1", size = 6598034, upload-time = "2025-03-12T19:26:40.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/44/255c7ecb8b6f3f730a37422736509c21cb1bf4da66cc060d872005bda9f5/litellm-1.63.7-py3-none-any.whl", hash = "sha256:fbdee39a894506c68f158c6b4e0079f9e9c023441fff7215e7b8e42162dba0a7", size = 6909807 }, + { url = "https://files.pythonhosted.org/packages/1e/44/255c7ecb8b6f3f730a37422736509c21cb1bf4da66cc060d872005bda9f5/litellm-1.63.7-py3-none-any.whl", hash = "sha256:fbdee39a894506c68f158c6b4e0079f9e9c023441fff7215e7b8e42162dba0a7", size = 6909807, upload-time = "2025-03-12T19:26:37.788Z" }, ] [[package]] name = "llvmlite" version = "0.44.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297 }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105 }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901 }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247 }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380 }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, ] [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240 }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164 }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206 }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144 }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124 }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520 }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016 }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884 }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690 }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418 }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092 }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231 }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798 }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195 }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243 }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197 }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392 }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103 }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224 }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913 }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441 }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580 }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493 }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679 }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691 }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075 }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680 }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253 }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651 }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315 }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149 }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095 }, +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/23/828d4cc7da96c611ec0ce6147bbcea2fdbde023dc995a165afa512399bbf/lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36", size = 8438217, upload-time = "2025-06-26T16:25:34.349Z" }, + { url = "https://files.pythonhosted.org/packages/f1/33/5ac521212c5bcb097d573145d54b2b4a3c9766cda88af5a0e91f66037c6e/lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25", size = 4590317, upload-time = "2025-06-26T16:25:38.103Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2e/45b7ca8bee304c07f54933c37afe7dd4d39ff61ba2757f519dcc71bc5d44/lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3", size = 5221628, upload-time = "2025-06-26T16:25:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/526d19f7eb2b85da1f62cffb2556f647b049ebe2a5aa8d4d41b1fb2c7d36/lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6", size = 4949429, upload-time = "2025-06-28T18:47:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cc/f6be27a5c656a43a5344e064d9ae004d4dcb1d3c9d4f323c8189ddfe4d13/lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b", size = 5087909, upload-time = "2025-06-28T18:47:22.834Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e6/8ec91b5bfbe6972458bc105aeb42088e50e4b23777170404aab5dfb0c62d/lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967", size = 5031713, upload-time = "2025-06-26T16:25:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/05e78e613840a40e5be3e40d892c48ad3e475804db23d4bad751b8cadb9b/lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e", size = 5232417, upload-time = "2025-06-26T16:25:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8c/6b306b3e35c59d5f0b32e3b9b6b3b0739b32c0dc42a295415ba111e76495/lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58", size = 4681443, upload-time = "2025-06-26T16:25:48.837Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/0bd96bece5f7eea14b7220476835a60d2b27f8e9ca99c175f37c085cb154/lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2", size = 5074542, upload-time = "2025-06-26T16:25:51.65Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3d/32103036287a8ca012d8518071f8852c68f2b3bfe048cef2a0202eb05910/lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851", size = 4729471, upload-time = "2025-06-26T16:25:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a8/7be5d17df12d637d81854bd8648cd329f29640a61e9a72a3f77add4a311b/lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f", size = 5256285, upload-time = "2025-06-26T16:25:56.997Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d0/6cb96174c25e0d749932557c8d51d60c6e292c877b46fae616afa23ed31a/lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c", size = 3612004, upload-time = "2025-06-26T16:25:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/ca/77/6ad43b165dfc6dead001410adeb45e88597b25185f4479b7ca3b16a5808f/lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816", size = 4003470, upload-time = "2025-06-26T16:26:01.655Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bc/4c50ec0eb14f932a18efc34fc86ee936a66c0eb5f2fe065744a2da8a68b2/lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab", size = 3682477, upload-time = "2025-06-26T16:26:03.808Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, ] [[package]] name = "lxml-stubs" version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778 } +sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584 }, + { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, ] [[package]] name = "lz4" version = "4.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/5a/945f5086326d569f14c84ac6f7fcc3229f0b9b1e8cc536b951fd53dfb9e1/lz4-4.4.4.tar.gz", hash = "sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda", size = 171884 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/5a/945f5086326d569f14c84ac6f7fcc3229f0b9b1e8cc536b951fd53dfb9e1/lz4-4.4.4.tar.gz", hash = "sha256:070fd0627ec4393011251a094e08ed9fdcc78cb4e7ab28f507638eee4e39abda", size = 171884, upload-time = "2025-04-01T22:55:58.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/e8/63843dc5ecb1529eb38e1761ceed04a0ad52a9ad8929ab8b7930ea2e4976/lz4-4.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f", size = 220898 }, - { url = "https://files.pythonhosted.org/packages/e4/94/c53de5f07c7dc11cf459aab2a1d754f5df5f693bfacbbe1e4914bfd02f1e/lz4-4.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550", size = 189685 }, - { url = "https://files.pythonhosted.org/packages/fe/59/c22d516dd0352f2a3415d1f665ccef2f3e74ecec3ca6a8f061a38f97d50d/lz4-4.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba", size = 1239225 }, - { url = "https://files.pythonhosted.org/packages/81/af/665685072e71f3f0e626221b7922867ec249cd8376aca761078c8f11f5da/lz4-4.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0", size = 1265881 }, - { url = "https://files.pythonhosted.org/packages/90/04/b4557ae381d3aa451388a29755cc410066f5e2f78c847f66f154f4520a68/lz4-4.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4", size = 1185593 }, - { url = "https://files.pythonhosted.org/packages/7b/e4/03636979f4e8bf92c557f998ca98ee4e6ef92e92eaf0ed6d3c7f2524e790/lz4-4.4.4-cp311-cp311-win32.whl", hash = "sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c", size = 88259 }, - { url = "https://files.pythonhosted.org/packages/07/f0/9efe53b4945441a5d2790d455134843ad86739855b7e6199977bf6dc8898/lz4-4.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78", size = 99916 }, - { url = "https://files.pythonhosted.org/packages/87/c8/1675527549ee174b9e1db089f7ddfbb962a97314657269b1e0344a5eaf56/lz4-4.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7", size = 89741 }, - { url = "https://files.pythonhosted.org/packages/f7/2d/5523b4fabe11cd98f040f715728d1932eb7e696bfe94391872a823332b94/lz4-4.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553", size = 220669 }, - { url = "https://files.pythonhosted.org/packages/91/06/1a5bbcacbfb48d8ee5b6eb3fca6aa84143a81d92946bdb5cd6b005f1863e/lz4-4.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e", size = 189661 }, - { url = "https://files.pythonhosted.org/packages/fa/08/39eb7ac907f73e11a69a11576a75a9e36406b3241c0ba41453a7eb842abb/lz4-4.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc", size = 1238775 }, - { url = "https://files.pythonhosted.org/packages/e9/26/05840fbd4233e8d23e88411a066ab19f1e9de332edddb8df2b6a95c7fddc/lz4-4.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad", size = 1265143 }, - { url = "https://files.pythonhosted.org/packages/b7/5d/5f2db18c298a419932f3ab2023deb689863cf8fd7ed875b1c43492479af2/lz4-4.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735", size = 1185032 }, - { url = "https://files.pythonhosted.org/packages/c4/e6/736ab5f128694b0f6aac58343bcf37163437ac95997276cd0be3ea4c3342/lz4-4.4.4-cp312-cp312-win32.whl", hash = "sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962", size = 88284 }, - { url = "https://files.pythonhosted.org/packages/40/b8/243430cb62319175070e06e3a94c4c7bd186a812e474e22148ae1290d47d/lz4-4.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12", size = 99918 }, - { url = "https://files.pythonhosted.org/packages/6c/e1/0686c91738f3e6c2e1a243e0fdd4371667c4d2e5009b0a3605806c2aa020/lz4-4.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62", size = 89736 }, + { url = "https://files.pythonhosted.org/packages/28/e8/63843dc5ecb1529eb38e1761ceed04a0ad52a9ad8929ab8b7930ea2e4976/lz4-4.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ddfc7194cd206496c445e9e5b0c47f970ce982c725c87bd22de028884125b68f", size = 220898, upload-time = "2025-04-01T22:55:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/e4/94/c53de5f07c7dc11cf459aab2a1d754f5df5f693bfacbbe1e4914bfd02f1e/lz4-4.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:714f9298c86f8e7278f1c6af23e509044782fa8220eb0260f8f8f1632f820550", size = 189685, upload-time = "2025-04-01T22:55:24.413Z" }, + { url = "https://files.pythonhosted.org/packages/fe/59/c22d516dd0352f2a3415d1f665ccef2f3e74ecec3ca6a8f061a38f97d50d/lz4-4.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8474c91de47733856c6686df3c4aca33753741da7e757979369c2c0d32918ba", size = 1239225, upload-time = "2025-04-01T22:55:25.737Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/665685072e71f3f0e626221b7922867ec249cd8376aca761078c8f11f5da/lz4-4.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80dd27d7d680ea02c261c226acf1d41de2fd77af4fb2da62b278a9376e380de0", size = 1265881, upload-time = "2025-04-01T22:55:26.817Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/b4557ae381d3aa451388a29755cc410066f5e2f78c847f66f154f4520a68/lz4-4.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b7d6dddfd01b49aedb940fdcaf32f41dc58c926ba35f4e31866aeec2f32f4f4", size = 1185593, upload-time = "2025-04-01T22:55:27.896Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e4/03636979f4e8bf92c557f998ca98ee4e6ef92e92eaf0ed6d3c7f2524e790/lz4-4.4.4-cp311-cp311-win32.whl", hash = "sha256:4134b9fd70ac41954c080b772816bb1afe0c8354ee993015a83430031d686a4c", size = 88259, upload-time = "2025-04-01T22:55:29.03Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/9efe53b4945441a5d2790d455134843ad86739855b7e6199977bf6dc8898/lz4-4.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:f5024d3ca2383470f7c4ef4d0ed8eabad0b22b23eeefde1c192cf1a38d5e9f78", size = 99916, upload-time = "2025-04-01T22:55:29.933Z" }, + { url = "https://files.pythonhosted.org/packages/87/c8/1675527549ee174b9e1db089f7ddfbb962a97314657269b1e0344a5eaf56/lz4-4.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:6ea715bb3357ea1665f77874cf8f55385ff112553db06f3742d3cdcec08633f7", size = 89741, upload-time = "2025-04-01T22:55:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/5523b4fabe11cd98f040f715728d1932eb7e696bfe94391872a823332b94/lz4-4.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:23ae267494fdd80f0d2a131beff890cf857f1b812ee72dbb96c3204aab725553", size = 220669, upload-time = "2025-04-01T22:55:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/1a5bbcacbfb48d8ee5b6eb3fca6aa84143a81d92946bdb5cd6b005f1863e/lz4-4.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fff9f3a1ed63d45cb6514bfb8293005dc4141341ce3500abdfeb76124c0b9b2e", size = 189661, upload-time = "2025-04-01T22:55:33.413Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/39eb7ac907f73e11a69a11576a75a9e36406b3241c0ba41453a7eb842abb/lz4-4.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ea7f07329f85a8eda4d8cf937b87f27f0ac392c6400f18bea2c667c8b7f8ecc", size = 1238775, upload-time = "2025-04-01T22:55:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/26/05840fbd4233e8d23e88411a066ab19f1e9de332edddb8df2b6a95c7fddc/lz4-4.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ccab8f7f7b82f9fa9fc3b0ba584d353bd5aa818d5821d77d5b9447faad2aaad", size = 1265143, upload-time = "2025-04-01T22:55:35.933Z" }, + { url = "https://files.pythonhosted.org/packages/b7/5d/5f2db18c298a419932f3ab2023deb689863cf8fd7ed875b1c43492479af2/lz4-4.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43e9d48b2daf80e486213128b0763deed35bbb7a59b66d1681e205e1702d735", size = 1185032, upload-time = "2025-04-01T22:55:37.454Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e6/736ab5f128694b0f6aac58343bcf37163437ac95997276cd0be3ea4c3342/lz4-4.4.4-cp312-cp312-win32.whl", hash = "sha256:33e01e18e4561b0381b2c33d58e77ceee850a5067f0ece945064cbaac2176962", size = 88284, upload-time = "2025-04-01T22:55:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/243430cb62319175070e06e3a94c4c7bd186a812e474e22148ae1290d47d/lz4-4.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d21d1a2892a2dcc193163dd13eaadabb2c1b803807a5117d8f8588b22eaf9f12", size = 99918, upload-time = "2025-04-01T22:55:39.628Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e1/0686c91738f3e6c2e1a243e0fdd4371667c4d2e5009b0a3605806c2aa020/lz4-4.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:2f4f2965c98ab254feddf6b5072854a6935adab7bc81412ec4fe238f07b85f62", size = 89736, upload-time = "2025-04-01T22:55:40.5Z" }, ] [[package]] @@ -2961,7 +2963,7 @@ dependencies = [ { name = "urllib3" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bc/cb60d02c00996839bbd87444a97d0ba5ac271b1a324001562afb8f685251/mailchimp_transactional-1.0.56-py3-none-any.whl", hash = "sha256:a76ea88b90a2d47d8b5134586aabbd3a96c459f6066d8886748ab59e50de36eb", size = 31660 }, + { url = "https://files.pythonhosted.org/packages/5f/bc/cb60d02c00996839bbd87444a97d0ba5ac271b1a324001562afb8f685251/mailchimp_transactional-1.0.56-py3-none-any.whl", hash = "sha256:a76ea88b90a2d47d8b5134586aabbd3a96c459f6066d8886748ab59e50de36eb", size = 31660, upload-time = "2024-02-01T18:39:19.717Z" }, ] [[package]] @@ -2971,18 +2973,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 }, + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, ] [[package]] name = "markdown" version = "3.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/28/c5441a6642681d92de56063fa7984df56f783d3f1eba518dc3e7a253b606/Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8", size = 349398 } +sdist = { url = "https://files.pythonhosted.org/packages/11/28/c5441a6642681d92de56063fa7984df56f783d3f1eba518dc3e7a253b606/Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8", size = 349398, upload-time = "2024-01-10T15:19:38.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/f4/f0031854de10a0bc7821ef9fca0b92ca0d7aa6fbfbf504c5473ba825e49c/Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", size = 103870 }, + { url = "https://files.pythonhosted.org/packages/42/f4/f0031854de10a0bc7821ef9fca0b92ca0d7aa6fbfbf504c5473ba825e49c/Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd", size = 103870, upload-time = "2024-01-10T15:19:36.071Z" }, ] [[package]] @@ -2992,37 +2994,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, ] [[package]] @@ -3032,81 +3034,95 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 }, + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "milvus-lite" -version = "2.4.12" +version = "2.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/64/3a/110e46db650ced604f97307e48e353726cfa6d26b1bf72acb81bbf07ecbd/milvus_lite-2.4.12-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:e8d4f7cdd5f731efd6faeee3715d280fd91a5f9b4d89312664d56401f65b1473", size = 19843871 }, - { url = "https://files.pythonhosted.org/packages/a5/a7/11c21f2d6f3299ad07af8142b007e4297ff12d4bdc53e1e1ba48f661954b/milvus_lite-2.4.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:20087663e7b4385050b7ad08f1f03404426d4c87b1ff91d5a8723eee7fd49e88", size = 17411635 }, - { url = "https://files.pythonhosted.org/packages/a8/cc/b6f465e984439adf24da0a8ff3035d5c9ece30b6ff19f9a53f73f9ef901a/milvus_lite-2.4.12-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a0f3a5ddbfd19f4a6b842b2fd3445693c796cde272b701a1646a94c1ac45d3d7", size = 35693118 }, - { url = "https://files.pythonhosted.org/packages/44/43/b3f6e9defd1f3927b972beac7abe3d5b4a3bdb287e3bad69618e2e76cf0a/milvus_lite-2.4.12-py3-none-manylinux2014_x86_64.whl", hash = "sha256:334037ebbab60243b5d8b43d54ca2f835d81d48c3cda0c6a462605e588deb05d", size = 45182549 }, + { url = "https://files.pythonhosted.org/packages/a9/b2/acc5024c8e8b6a0b034670b8e8af306ebd633ede777dcbf557eac4785937/milvus_lite-2.5.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6b014453200ba977be37ba660cb2d021030375fa6a35bc53c2e1d92980a0c512", size = 27934713, upload-time = "2025-06-30T04:23:37.028Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2e/746f5bb1d6facd1e73eb4af6dd5efda11125b0f29d7908a097485ca6cad9/milvus_lite-2.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2e031088bf308afe5f8567850412d618cfb05a65238ed1a6117f60decccc95a", size = 24421451, upload-time = "2025-06-30T04:23:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/3d1fee5c16c7661cf53977067a34820f7269ed8ba99fe9cf35efc1700866/milvus_lite-2.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a13277e9bacc6933dea172e42231f7e6135bd3bdb073dd2688ee180418abd8d9", size = 45337093, upload-time = "2025-06-30T04:24:06.706Z" }, + { url = "https://files.pythonhosted.org/packages/d3/82/41d9b80f09b82e066894d9b508af07b7b0fa325ce0322980674de49106a0/milvus_lite-2.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25ce13f4b8d46876dd2b7ac8563d7d8306da7ff3999bb0d14b116b30f71d706c", size = 55263911, upload-time = "2025-06-30T04:24:19.434Z" }, ] [[package]] name = "mmh3" version = "5.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/09/fda7af7fe65928262098382e3bf55950cfbf67d30bf9e47731bf862161e9/mmh3-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b529dcda3f951ff363a51d5866bc6d63cf57f1e73e8961f864ae5010647079d", size = 56098 }, - { url = "https://files.pythonhosted.org/packages/0c/ab/84c7bc3f366d6f3bd8b5d9325a10c367685bc17c26dac4c068e2001a4671/mmh3-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db1079b3ace965e562cdfc95847312f9273eb2ad3ebea983435c8423e06acd7", size = 40513 }, - { url = "https://files.pythonhosted.org/packages/4f/21/25ea58ca4a652bdc83d1528bec31745cce35802381fb4fe3c097905462d2/mmh3-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22d31e3a0ff89b8eb3b826d6fc8e19532998b2aa6b9143698043a1268da413e1", size = 40112 }, - { url = "https://files.pythonhosted.org/packages/bd/78/4f12f16ae074ddda6f06745254fdb50f8cf3c85b0bbf7eaca58bed84bf58/mmh3-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2139bfbd354cd6cb0afed51c4b504f29bcd687a3b1460b7e89498329cc28a894", size = 102632 }, - { url = "https://files.pythonhosted.org/packages/48/11/8f09dc999cf2a09b6138d8d7fc734efb7b7bfdd9adb9383380941caadff0/mmh3-5.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c8105c6a435bc2cd6ea2ef59558ab1a2976fd4a4437026f562856d08996673a", size = 108884 }, - { url = "https://files.pythonhosted.org/packages/bd/91/e59a66538a3364176f6c3f7620eee0ab195bfe26f89a95cbcc7a1fb04b28/mmh3-5.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57730067174a7f36fcd6ce012fe359bd5510fdaa5fe067bc94ed03e65dafb769", size = 106835 }, - { url = "https://files.pythonhosted.org/packages/25/14/b85836e21ab90e5cddb85fe79c494ebd8f81d96a87a664c488cc9277668b/mmh3-5.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde80eb196d7fdc765a318604ded74a4378f02c5b46c17aa48a27d742edaded2", size = 93688 }, - { url = "https://files.pythonhosted.org/packages/ac/aa/8bc964067df9262740c95e4cde2d19f149f2224f426654e14199a9e47df6/mmh3-5.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c8eddcb441abddeb419c16c56fd74b3e2df9e57f7aa2903221996718435c7a", size = 101569 }, - { url = "https://files.pythonhosted.org/packages/70/b6/1fb163cbf919046a64717466c00edabebece3f95c013853fec76dbf2df92/mmh3-5.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:99e07e4acafbccc7a28c076a847fb060ffc1406036bc2005acb1b2af620e53c3", size = 98483 }, - { url = "https://files.pythonhosted.org/packages/70/49/ba64c050dd646060f835f1db6b2cd60a6485f3b0ea04976e7a29ace7312e/mmh3-5.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e25ba5b530e9a7d65f41a08d48f4b3fedc1e89c26486361166a5544aa4cad33", size = 96496 }, - { url = "https://files.pythonhosted.org/packages/9e/07/f2751d6a0b535bb865e1066e9c6b80852571ef8d61bce7eb44c18720fbfc/mmh3-5.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bb9bf7475b4d99156ce2f0cf277c061a17560c8c10199c910a680869a278ddc7", size = 105109 }, - { url = "https://files.pythonhosted.org/packages/b7/02/30360a5a66f7abba44596d747cc1e6fb53136b168eaa335f63454ab7bb79/mmh3-5.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a1b0878dd281ea3003368ab53ff6f568e175f1b39f281df1da319e58a19c23a", size = 98231 }, - { url = "https://files.pythonhosted.org/packages/8c/60/8526b0c750ff4d7ae1266e68b795f14b97758a1d9fcc19f6ecabf9c55656/mmh3-5.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:25f565093ac8b8aefe0f61f8f95c9a9d11dd69e6a9e9832ff0d293511bc36258", size = 97548 }, - { url = "https://files.pythonhosted.org/packages/6d/4c/26e1222aca65769280d5427a1ce5875ef4213449718c8f03958d0bf91070/mmh3-5.1.0-cp311-cp311-win32.whl", hash = "sha256:1e3554d8792387eac73c99c6eaea0b3f884e7130eb67986e11c403e4f9b6d372", size = 40810 }, - { url = "https://files.pythonhosted.org/packages/98/d5/424ba95062d1212ea615dc8debc8d57983f2242d5e6b82e458b89a117a1e/mmh3-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad777a48197882492af50bf3098085424993ce850bdda406a358b6ab74be759", size = 41476 }, - { url = "https://files.pythonhosted.org/packages/bd/08/0315ccaf087ba55bb19a6dd3b1e8acd491e74ce7f5f9c4aaa06a90d66441/mmh3-5.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f29dc4efd99bdd29fe85ed6c81915b17b2ef2cf853abf7213a48ac6fb3eaabe1", size = 38880 }, - { url = "https://files.pythonhosted.org/packages/f4/47/e5f452bdf16028bfd2edb4e2e35d0441e4a4740f30e68ccd4cfd2fb2c57e/mmh3-5.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45712987367cb9235026e3cbf4334670522a97751abfd00b5bc8bfa022c3311d", size = 56152 }, - { url = "https://files.pythonhosted.org/packages/60/38/2132d537dc7a7fdd8d2e98df90186c7fcdbd3f14f95502a24ba443c92245/mmh3-5.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1020735eb35086ab24affbea59bb9082f7f6a0ad517cb89f0fc14f16cea4dae", size = 40564 }, - { url = "https://files.pythonhosted.org/packages/c0/2a/c52cf000581bfb8d94794f58865658e7accf2fa2e90789269d4ae9560b16/mmh3-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:babf2a78ce5513d120c358722a2e3aa7762d6071cd10cede026f8b32452be322", size = 40104 }, - { url = "https://files.pythonhosted.org/packages/83/33/30d163ce538c54fc98258db5621447e3ab208d133cece5d2577cf913e708/mmh3-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4f47f58cd5cbef968c84a7c1ddc192fef0a36b48b0b8a3cb67354531aa33b00", size = 102634 }, - { url = "https://files.pythonhosted.org/packages/94/5c/5a18acb6ecc6852be2d215c3d811aa61d7e425ab6596be940877355d7f3e/mmh3-5.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2044a601c113c981f2c1e14fa33adc9b826c9017034fe193e9eb49a6882dbb06", size = 108888 }, - { url = "https://files.pythonhosted.org/packages/1f/f6/11c556324c64a92aa12f28e221a727b6e082e426dc502e81f77056f6fc98/mmh3-5.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94d999c9f2eb2da44d7c2826d3fbffdbbbbcde8488d353fee7c848ecc42b968", size = 106968 }, - { url = "https://files.pythonhosted.org/packages/5d/61/ca0c196a685aba7808a5c00246f17b988a9c4f55c594ee0a02c273e404f3/mmh3-5.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a015dcb24fa0c7a78f88e9419ac74f5001c1ed6a92e70fd1803f74afb26a4c83", size = 93771 }, - { url = "https://files.pythonhosted.org/packages/b4/55/0927c33528710085ee77b808d85bbbafdb91a1db7c8eaa89cac16d6c513e/mmh3-5.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:457da019c491a2d20e2022c7d4ce723675e4c081d9efc3b4d8b9f28a5ea789bd", size = 101726 }, - { url = "https://files.pythonhosted.org/packages/49/39/a92c60329fa470f41c18614a93c6cd88821412a12ee78c71c3f77e1cfc2d/mmh3-5.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71408579a570193a4ac9c77344d68ddefa440b00468a0b566dcc2ba282a9c559", size = 98523 }, - { url = "https://files.pythonhosted.org/packages/81/90/26adb15345af8d9cf433ae1b6adcf12e0a4cad1e692de4fa9f8e8536c5ae/mmh3-5.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8b3a04bc214a6e16c81f02f855e285c6df274a2084787eeafaa45f2fbdef1b63", size = 96628 }, - { url = "https://files.pythonhosted.org/packages/8a/4d/340d1e340df972a13fd4ec84c787367f425371720a1044220869c82364e9/mmh3-5.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:832dae26a35514f6d3c1e267fa48e8de3c7b978afdafa0529c808ad72e13ada3", size = 105190 }, - { url = "https://files.pythonhosted.org/packages/d3/7c/65047d1cccd3782d809936db446430fc7758bda9def5b0979887e08302a2/mmh3-5.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf658a61fc92ef8a48945ebb1076ef4ad74269e353fffcb642dfa0890b13673b", size = 98439 }, - { url = "https://files.pythonhosted.org/packages/72/d2/3c259d43097c30f062050f7e861075099404e8886b5d4dd3cebf180d6e02/mmh3-5.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3313577453582b03383731b66447cdcdd28a68f78df28f10d275d7d19010c1df", size = 97780 }, - { url = "https://files.pythonhosted.org/packages/29/29/831ea8d4abe96cdb3e28b79eab49cac7f04f9c6b6e36bfc686197ddba09d/mmh3-5.1.0-cp312-cp312-win32.whl", hash = "sha256:1d6508504c531ab86c4424b5a5ff07c1132d063863339cf92f6657ff7a580f76", size = 40835 }, - { url = "https://files.pythonhosted.org/packages/12/dd/7cbc30153b73f08eeac43804c1dbc770538a01979b4094edbe1a4b8eb551/mmh3-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa75981fcdf3f21759d94f2c81b6a6e04a49dfbcdad88b152ba49b8e20544776", size = 41509 }, - { url = "https://files.pythonhosted.org/packages/80/9d/627375bab4c90dd066093fc2c9a26b86f87e26d980dbf71667b44cbee3eb/mmh3-5.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4c1a76808dfea47f7407a0b07aaff9087447ef6280716fd0783409b3088bb3c", size = 38888 }, +sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728, upload-time = "2025-01-25T08:39:43.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/09/fda7af7fe65928262098382e3bf55950cfbf67d30bf9e47731bf862161e9/mmh3-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b529dcda3f951ff363a51d5866bc6d63cf57f1e73e8961f864ae5010647079d", size = 56098, upload-time = "2025-01-25T08:38:22.917Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/84c7bc3f366d6f3bd8b5d9325a10c367685bc17c26dac4c068e2001a4671/mmh3-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db1079b3ace965e562cdfc95847312f9273eb2ad3ebea983435c8423e06acd7", size = 40513, upload-time = "2025-01-25T08:38:25.079Z" }, + { url = "https://files.pythonhosted.org/packages/4f/21/25ea58ca4a652bdc83d1528bec31745cce35802381fb4fe3c097905462d2/mmh3-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22d31e3a0ff89b8eb3b826d6fc8e19532998b2aa6b9143698043a1268da413e1", size = 40112, upload-time = "2025-01-25T08:38:25.947Z" }, + { url = "https://files.pythonhosted.org/packages/bd/78/4f12f16ae074ddda6f06745254fdb50f8cf3c85b0bbf7eaca58bed84bf58/mmh3-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2139bfbd354cd6cb0afed51c4b504f29bcd687a3b1460b7e89498329cc28a894", size = 102632, upload-time = "2025-01-25T08:38:26.939Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/8f09dc999cf2a09b6138d8d7fc734efb7b7bfdd9adb9383380941caadff0/mmh3-5.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c8105c6a435bc2cd6ea2ef59558ab1a2976fd4a4437026f562856d08996673a", size = 108884, upload-time = "2025-01-25T08:38:29.159Z" }, + { url = "https://files.pythonhosted.org/packages/bd/91/e59a66538a3364176f6c3f7620eee0ab195bfe26f89a95cbcc7a1fb04b28/mmh3-5.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57730067174a7f36fcd6ce012fe359bd5510fdaa5fe067bc94ed03e65dafb769", size = 106835, upload-time = "2025-01-25T08:38:33.04Z" }, + { url = "https://files.pythonhosted.org/packages/25/14/b85836e21ab90e5cddb85fe79c494ebd8f81d96a87a664c488cc9277668b/mmh3-5.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde80eb196d7fdc765a318604ded74a4378f02c5b46c17aa48a27d742edaded2", size = 93688, upload-time = "2025-01-25T08:38:34.987Z" }, + { url = "https://files.pythonhosted.org/packages/ac/aa/8bc964067df9262740c95e4cde2d19f149f2224f426654e14199a9e47df6/mmh3-5.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c8eddcb441abddeb419c16c56fd74b3e2df9e57f7aa2903221996718435c7a", size = 101569, upload-time = "2025-01-25T08:38:35.983Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/1fb163cbf919046a64717466c00edabebece3f95c013853fec76dbf2df92/mmh3-5.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:99e07e4acafbccc7a28c076a847fb060ffc1406036bc2005acb1b2af620e53c3", size = 98483, upload-time = "2025-01-25T08:38:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/ba64c050dd646060f835f1db6b2cd60a6485f3b0ea04976e7a29ace7312e/mmh3-5.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e25ba5b530e9a7d65f41a08d48f4b3fedc1e89c26486361166a5544aa4cad33", size = 96496, upload-time = "2025-01-25T08:38:39.257Z" }, + { url = "https://files.pythonhosted.org/packages/9e/07/f2751d6a0b535bb865e1066e9c6b80852571ef8d61bce7eb44c18720fbfc/mmh3-5.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bb9bf7475b4d99156ce2f0cf277c061a17560c8c10199c910a680869a278ddc7", size = 105109, upload-time = "2025-01-25T08:38:40.395Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/30360a5a66f7abba44596d747cc1e6fb53136b168eaa335f63454ab7bb79/mmh3-5.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a1b0878dd281ea3003368ab53ff6f568e175f1b39f281df1da319e58a19c23a", size = 98231, upload-time = "2025-01-25T08:38:42.141Z" }, + { url = "https://files.pythonhosted.org/packages/8c/60/8526b0c750ff4d7ae1266e68b795f14b97758a1d9fcc19f6ecabf9c55656/mmh3-5.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:25f565093ac8b8aefe0f61f8f95c9a9d11dd69e6a9e9832ff0d293511bc36258", size = 97548, upload-time = "2025-01-25T08:38:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4c/26e1222aca65769280d5427a1ce5875ef4213449718c8f03958d0bf91070/mmh3-5.1.0-cp311-cp311-win32.whl", hash = "sha256:1e3554d8792387eac73c99c6eaea0b3f884e7130eb67986e11c403e4f9b6d372", size = 40810, upload-time = "2025-01-25T08:38:45.143Z" }, + { url = "https://files.pythonhosted.org/packages/98/d5/424ba95062d1212ea615dc8debc8d57983f2242d5e6b82e458b89a117a1e/mmh3-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad777a48197882492af50bf3098085424993ce850bdda406a358b6ab74be759", size = 41476, upload-time = "2025-01-25T08:38:46.029Z" }, + { url = "https://files.pythonhosted.org/packages/bd/08/0315ccaf087ba55bb19a6dd3b1e8acd491e74ce7f5f9c4aaa06a90d66441/mmh3-5.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f29dc4efd99bdd29fe85ed6c81915b17b2ef2cf853abf7213a48ac6fb3eaabe1", size = 38880, upload-time = "2025-01-25T08:38:47.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/47/e5f452bdf16028bfd2edb4e2e35d0441e4a4740f30e68ccd4cfd2fb2c57e/mmh3-5.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45712987367cb9235026e3cbf4334670522a97751abfd00b5bc8bfa022c3311d", size = 56152, upload-time = "2025-01-25T08:38:47.902Z" }, + { url = "https://files.pythonhosted.org/packages/60/38/2132d537dc7a7fdd8d2e98df90186c7fcdbd3f14f95502a24ba443c92245/mmh3-5.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1020735eb35086ab24affbea59bb9082f7f6a0ad517cb89f0fc14f16cea4dae", size = 40564, upload-time = "2025-01-25T08:38:48.839Z" }, + { url = "https://files.pythonhosted.org/packages/c0/2a/c52cf000581bfb8d94794f58865658e7accf2fa2e90789269d4ae9560b16/mmh3-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:babf2a78ce5513d120c358722a2e3aa7762d6071cd10cede026f8b32452be322", size = 40104, upload-time = "2025-01-25T08:38:49.773Z" }, + { url = "https://files.pythonhosted.org/packages/83/33/30d163ce538c54fc98258db5621447e3ab208d133cece5d2577cf913e708/mmh3-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4f47f58cd5cbef968c84a7c1ddc192fef0a36b48b0b8a3cb67354531aa33b00", size = 102634, upload-time = "2025-01-25T08:38:51.5Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/5a18acb6ecc6852be2d215c3d811aa61d7e425ab6596be940877355d7f3e/mmh3-5.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2044a601c113c981f2c1e14fa33adc9b826c9017034fe193e9eb49a6882dbb06", size = 108888, upload-time = "2025-01-25T08:38:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/11c556324c64a92aa12f28e221a727b6e082e426dc502e81f77056f6fc98/mmh3-5.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94d999c9f2eb2da44d7c2826d3fbffdbbbbcde8488d353fee7c848ecc42b968", size = 106968, upload-time = "2025-01-25T08:38:54.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/ca0c196a685aba7808a5c00246f17b988a9c4f55c594ee0a02c273e404f3/mmh3-5.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a015dcb24fa0c7a78f88e9419ac74f5001c1ed6a92e70fd1803f74afb26a4c83", size = 93771, upload-time = "2025-01-25T08:38:55.576Z" }, + { url = "https://files.pythonhosted.org/packages/b4/55/0927c33528710085ee77b808d85bbbafdb91a1db7c8eaa89cac16d6c513e/mmh3-5.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:457da019c491a2d20e2022c7d4ce723675e4c081d9efc3b4d8b9f28a5ea789bd", size = 101726, upload-time = "2025-01-25T08:38:56.654Z" }, + { url = "https://files.pythonhosted.org/packages/49/39/a92c60329fa470f41c18614a93c6cd88821412a12ee78c71c3f77e1cfc2d/mmh3-5.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71408579a570193a4ac9c77344d68ddefa440b00468a0b566dcc2ba282a9c559", size = 98523, upload-time = "2025-01-25T08:38:57.662Z" }, + { url = "https://files.pythonhosted.org/packages/81/90/26adb15345af8d9cf433ae1b6adcf12e0a4cad1e692de4fa9f8e8536c5ae/mmh3-5.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8b3a04bc214a6e16c81f02f855e285c6df274a2084787eeafaa45f2fbdef1b63", size = 96628, upload-time = "2025-01-25T08:38:59.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4d/340d1e340df972a13fd4ec84c787367f425371720a1044220869c82364e9/mmh3-5.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:832dae26a35514f6d3c1e267fa48e8de3c7b978afdafa0529c808ad72e13ada3", size = 105190, upload-time = "2025-01-25T08:39:00.483Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/65047d1cccd3782d809936db446430fc7758bda9def5b0979887e08302a2/mmh3-5.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf658a61fc92ef8a48945ebb1076ef4ad74269e353fffcb642dfa0890b13673b", size = 98439, upload-time = "2025-01-25T08:39:01.484Z" }, + { url = "https://files.pythonhosted.org/packages/72/d2/3c259d43097c30f062050f7e861075099404e8886b5d4dd3cebf180d6e02/mmh3-5.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3313577453582b03383731b66447cdcdd28a68f78df28f10d275d7d19010c1df", size = 97780, upload-time = "2025-01-25T08:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/29/29/831ea8d4abe96cdb3e28b79eab49cac7f04f9c6b6e36bfc686197ddba09d/mmh3-5.1.0-cp312-cp312-win32.whl", hash = "sha256:1d6508504c531ab86c4424b5a5ff07c1132d063863339cf92f6657ff7a580f76", size = 40835, upload-time = "2025-01-25T08:39:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/7cbc30153b73f08eeac43804c1dbc770538a01979b4094edbe1a4b8eb551/mmh3-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa75981fcdf3f21759d94f2c81b6a6e04a49dfbcdad88b152ba49b8e20544776", size = 41509, upload-time = "2025-01-25T08:39:04.284Z" }, + { url = "https://files.pythonhosted.org/packages/80/9d/627375bab4c90dd066093fc2c9a26b86f87e26d980dbf71667b44cbee3eb/mmh3-5.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4c1a76808dfea47f7407a0b07aaff9087447ef6280716fd0783409b3088bb3c", size = 38888, upload-time = "2025-01-25T08:39:05.174Z" }, +] + +[[package]] +name = "mo-vector" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pymysql" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/03/2ef4de1c8d970288f018b6b63439563336c51f26f57706dc51e4c395fdbe/mo_vector-0.1.13.tar.gz", hash = "sha256:8526c37e99157a0c9866bf3868600e877980464eccb212f8ea71971c0630eb69", size = 16926, upload-time = "2025-06-18T09:27:27.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e7/514f5cf5909f96adf09b78146a9e5c92f82abcc212bc3f88456bf2640c23/mo_vector-0.1.13-py3-none-any.whl", hash = "sha256:f7d619acc3e92ed59631e6b3a12508240e22cf428c87daf022c0d87fbd5da459", size = 20091, upload-time = "2025-06-18T09:27:26.899Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] @@ -3118,9 +3134,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358 }, + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, ] [[package]] @@ -3130,9 +3146,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] [[package]] @@ -3146,107 +3162,110 @@ dependencies = [ { name = "requests" }, { name = "requests-oauthlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 } +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 }, + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, ] [[package]] name = "multidict" -version = "6.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515 }, - { url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609 }, - { url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871 }, - { url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661 }, - { url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422 }, - { url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447 }, - { url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455 }, - { url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666 }, - { url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392 }, - { url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969 }, - { url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433 }, - { url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418 }, - { url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042 }, - { url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280 }, - { url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322 }, - { url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070 }, - { url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667 }, - { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 }, - { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 }, - { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 }, - { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 }, - { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 }, - { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 }, - { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 }, - { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 }, - { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 }, - { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 }, - { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 }, - { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 }, - { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 }, - { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 }, - { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 }, - { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 }, - { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 }, - { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 }, +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, + { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, + { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, + { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, + { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, + { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, + { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, ] [[package]] name = "mypy-boto3-bedrock-runtime" -version = "1.38.4" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/55/56ce6f23d7fb98ce5b8a4261f089890bc94250666ea7089539dab55f6c25/mypy_boto3_bedrock_runtime-1.38.4.tar.gz", hash = "sha256:315a5f84c014c54e5406fdb80b030aba5cc79eb27982aff3d09ef331fb2cdd4d", size = 26169 } +sdist = { url = "https://files.pythonhosted.org/packages/c6/6d/65c684441a91cd16f00e442a7ebb34bba5ee335ba8bb9ec5ad8f08e71e27/mypy_boto3_bedrock_runtime-1.39.0.tar.gz", hash = "sha256:f3eb0972bd3801013470cffd9dd094ff93ddcd6fae7ca17ec5bad1e357ab8117", size = 26901, upload-time = "2025-06-30T19:34:15.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/eb/3015c6504540ca4888789ee14f47590c0340b748a33b059eeb6a48b406bb/mypy_boto3_bedrock_runtime-1.38.4-py3-none-any.whl", hash = "sha256:af14320532e9b798095129a3307f4b186ba80258917bb31410cda7f423592d72", size = 31858 }, + { url = "https://files.pythonhosted.org/packages/05/92/ed01279bf155a1afe78a57d8e34f22604be66f59cb2b7c2f26e73715ced5/mypy_boto3_bedrock_runtime-1.39.0-py3-none-any.whl", hash = "sha256:2925d76b72ec77a7dc2169a0483c36567078de74cf2fcfff084e87b0e2c5ca8b", size = 32623, upload-time = "2025-06-30T19:34:13.663Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nest-asyncio" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] @@ -3259,9 +3278,9 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691 } +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442 }, + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, ] [[package]] @@ -3272,76 +3291,74 @@ dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, - { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, - { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, - { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, - { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, - { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626 }, - { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287 }, - { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928 }, - { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115 }, - { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929 }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, ] [[package]] name = "numexpr" -version = "2.10.2" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/67/c7415cf04ebe418193cfd6595ae03e3a64d76dac7b9c010098b39cc7992e/numexpr-2.10.2.tar.gz", hash = "sha256:b0aff6b48ebc99d2f54f27b5f73a58cb92fde650aeff1b397c71c8788b4fff1a", size = 106787 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/8f/2cc977e91adbfbcdb6b49fdb9147e1d1c7566eb2c0c1e737e9a47020b5ca/numexpr-2.11.0.tar.gz", hash = "sha256:75b2c01a4eda2e7c357bc67a3f5c3dd76506c15b5fd4dc42845ef2e182181bad", size = 108960, upload-time = "2025-06-09T11:05:56.79Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b7/f25d6166f92ef23737c1c90416144492a664f0a56510d90f7c6577c2cd14/numexpr-2.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b360eb8d392483410fe6a3d5a7144afa298c9a0aa3e9fe193e89590b47dd477", size = 145055 }, - { url = "https://files.pythonhosted.org/packages/66/64/428361ea6415826332f38ef2dd5c3abf4e7e601f033bfc9be68b680cb765/numexpr-2.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9a42f5c24880350d88933c4efee91b857c378aaea7e8b86221fff569069841e", size = 134743 }, - { url = "https://files.pythonhosted.org/packages/3f/fb/639ec91d2ea7b4a5d66e26e8ef8e06b020c8e9b9ebaf3bab7b0a9bee472e/numexpr-2.10.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83fcb11988b57cc25b028a36d285287d706d1f536ebf2662ea30bd990e0de8b9", size = 410397 }, - { url = "https://files.pythonhosted.org/packages/89/5a/0f5c5b8a3a6d34eeecb30d0e2f722d50b9b38c0e175937e7c6268ffab997/numexpr-2.10.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4213a92efa9770bc28e3792134e27c7e5c7e97068bdfb8ba395baebbd12f991b", size = 398902 }, - { url = "https://files.pythonhosted.org/packages/a2/d5/ec734e735eba5a753efed5be3707ee7447ebd371772f8081b65a4153fb97/numexpr-2.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebdbef5763ca057eea0c2b5698e4439d084a0505d9d6e94f4804f26e8890c45e", size = 1380354 }, - { url = "https://files.pythonhosted.org/packages/30/51/406e572531d817480bd612ee08239a36ee82865fea02fce569f15631f4ee/numexpr-2.10.2-cp311-cp311-win32.whl", hash = "sha256:3bf01ec502d89944e49e9c1b5cc7c7085be8ca2eb9dd46a0eafd218afbdbd5f5", size = 151938 }, - { url = "https://files.pythonhosted.org/packages/04/32/5882ed1dbd96234f327a73316a481add151ff827cfaf2ea24fb4d5ad04db/numexpr-2.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e2d0ae24b0728e4bc3f1d3f33310340d67321d36d6043f7ce26897f4f1042db0", size = 144961 }, - { url = "https://files.pythonhosted.org/packages/2b/96/d5053dea06d8298ae8052b4b049cbf8ef74998e28d57166cc27b8ae909e2/numexpr-2.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5323a46e75832334f1af86da1ef6ff0add00fbacdd266250be872b438bdf2be", size = 145029 }, - { url = "https://files.pythonhosted.org/packages/3e/3c/fcd5a812ed5dda757b2d9ef2764a3e1cca6f6d1f02dbf113dc23a2c7702a/numexpr-2.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a42963bd4c62d8afa4f51e7974debfa39a048383f653544ab54f50a2f7ec6c42", size = 134851 }, - { url = "https://files.pythonhosted.org/packages/0a/52/0ed3b306d8c9944129bce97fec73a2caff13adbd7e1df148d546d7eb2d4d/numexpr-2.10.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5191ba8f2975cb9703afc04ae845a929e193498c0e8bcd408ecb147b35978470", size = 411837 }, - { url = "https://files.pythonhosted.org/packages/7d/9c/6b671dd3fb67d7e7da93cb76b7c5277743f310a216b7856bb18776bb3371/numexpr-2.10.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97298b14f0105a794bea06fd9fbc5c423bd3ff4d88cbc618860b83eb7a436ad6", size = 400577 }, - { url = "https://files.pythonhosted.org/packages/ea/4d/a167d1a215fe10ce58c45109f2869fd13aa0eef66f7e8c69af68be45d436/numexpr-2.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9d7805ccb6be2d3b0f7f6fad3707a09ac537811e8e9964f4074d28cb35543db", size = 1381735 }, - { url = "https://files.pythonhosted.org/packages/c1/d4/17e4434f989e4917d31cbd88a043e1c9c16958149cf43fa622987111392b/numexpr-2.10.2-cp312-cp312-win32.whl", hash = "sha256:cb845b2d4f9f8ef0eb1c9884f2b64780a85d3b5ae4eeb26ae2b0019f489cd35e", size = 152102 }, - { url = "https://files.pythonhosted.org/packages/b8/25/9ae599994076ef2a42d35ff6b0430da002647f212567851336a6c7b132d6/numexpr-2.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:57b59cbb5dcce4edf09cd6ce0b57ff60312479930099ca8d944c2fac896a1ead", size = 145061 }, + { url = "https://files.pythonhosted.org/packages/d8/d1/1cf8137990b3f3d445556ed63b9bc347aec39bde8c41146b02d3b35c1adc/numexpr-2.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:450eba3c93c3e3e8070566ad8d70590949d6e574b1c960bf68edd789811e7da8", size = 147535, upload-time = "2025-06-09T11:05:08.929Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/bac7649d043f47c7c14c797efe60dbd19476468a149399cd706fe2e47f8c/numexpr-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0eb88dbac8a7e61ee433006d0ddfd6eb921f5c6c224d1b50855bc98fb304c44", size = 136710, upload-time = "2025-06-09T11:05:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/c88fc34d82d23c66ea0b78b00a1fb3b64048e0f7ac7791b2cd0d2a4ce14d/numexpr-2.11.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a194e3684b3553ea199c3f4837f422a521c7e2f0cce13527adc3a6b4049f9e7c", size = 411169, upload-time = "2025-06-09T11:05:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8d/4d78dad430b41d836146f9e6f545f5c4f7d1972a6aa427d8570ab232bf16/numexpr-2.11.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f677668ab2bb2452fee955af3702fbb3b71919e61e4520762b1e5f54af59c0d8", size = 401671, upload-time = "2025-06-09T11:05:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/414670eb41a82b78bd09769a4f5fb49a934f9b3990957f02c833637a511e/numexpr-2.11.0-cp311-cp311-win32.whl", hash = "sha256:7d9e76a77c9644fbd60da3984e516ead5b84817748c2da92515cd36f1941a04d", size = 153159, upload-time = "2025-06-09T11:05:14.452Z" }, + { url = "https://files.pythonhosted.org/packages/0c/97/8d00ca9b36f3ac68a8fd85e930ab0c9448d8c9ca7ce195ee75c188dabd45/numexpr-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7163b488bfdcd13c300a8407c309e4cee195ef95d07facf5ac2678d66c988805", size = 146224, upload-time = "2025-06-09T11:05:15.877Z" }, + { url = "https://files.pythonhosted.org/packages/38/45/7a0e5a0b800d92e73825494ac695fa05a52c7fc7088d69a336880136b437/numexpr-2.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4229060be866813122385c608bbd3ea48fe0b33e91f2756810d28c1cdbfc98f1", size = 147494, upload-time = "2025-06-09T11:05:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/74/46/3a26b84e44f4739ec98de0ede4b95b4b8096f721e22d0e97517eeb02017e/numexpr-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:097aa8835d32d6ac52f2be543384019b4b134d1fb67998cbfc4271155edfe54a", size = 136832, upload-time = "2025-06-09T11:05:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/e3076ff25d4a108b47640c169c0a64811748c43b63d9cc052ea56de1631e/numexpr-2.11.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f082321c244ff5d0e252071fb2c4fe02063a45934144a1456a5370ca139bec2", size = 412618, upload-time = "2025-06-09T11:05:20.093Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/15e0e077a004db0edd530da96c60c948689c888c464ee5d14b82405ebd86/numexpr-2.11.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7a19435ca3d7dd502b8d8dce643555eb1b6013989e3f7577857289f6db6be16", size = 403363, upload-time = "2025-06-09T11:05:21.217Z" }, + { url = "https://files.pythonhosted.org/packages/10/14/f22afb3a7ae41d03ba87f62d00fbcfb76389f9cc91b7a82593c39c509318/numexpr-2.11.0-cp312-cp312-win32.whl", hash = "sha256:f326218262c8d8537887cc4bbd613c8409d62f2cac799835c0360e0d9cefaa5c", size = 153307, upload-time = "2025-06-09T11:05:22.855Z" }, + { url = "https://files.pythonhosted.org/packages/18/70/abc585269424582b3cd6db261e33b2ec96b5d4971da3edb29fc9b62a8926/numexpr-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a184e5930c77ab91dd9beee4df403b825cd9dfc4e9ba4670d31c9fcb4e2c08e", size = 146337, upload-time = "2025-06-09T11:05:23.976Z" }, ] [[package]] name = "numpy" version = "1.26.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, ] [[package]] name = "oauthlib" -version = "3.2.2" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] @@ -3351,15 +3368,15 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "defusedxml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003f7ce3304f524774adda96e6aaab30ea79fd8fda7934/odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", size = 717045 } +sdist = { url = "https://files.pythonhosted.org/packages/97/73/8ade73f6749177003f7ce3304f524774adda96e6aaab30ea79fd8fda7934/odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec", size = 717045, upload-time = "2020-01-18T16:55:48.852Z" } [[package]] name = "olefile" version = "0.47" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240 } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565 }, + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, ] [[package]] @@ -3375,14 +3392,14 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/08/c008711d1b92ff1272f4fea0fbee57723171f161d42e5c680625535280af/onnxruntime-1.22.0-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:8d6725c5b9a681d8fe72f2960c191a96c256367887d076b08466f52b4e0991df", size = 34282151 }, - { url = "https://files.pythonhosted.org/packages/3e/8b/22989f6b59bc4ad1324f07a945c80b9ab825f0a581ad7a6064b93716d9b7/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef17d665a917866d1f68f09edc98223b9a27e6cb167dec69da4c66484ad12fd", size = 14446302 }, - { url = "https://files.pythonhosted.org/packages/7a/d5/aa83d084d05bc8f6cf8b74b499c77431ffd6b7075c761ec48ec0c161a47f/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b978aa63a9a22095479c38371a9b359d4c15173cbb164eaad5f2cd27d666aa65", size = 16393496 }, - { url = "https://files.pythonhosted.org/packages/89/a5/1c6c10322201566015183b52ef011dfa932f5dd1b278de8d75c3b948411d/onnxruntime-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:03d3ef7fb11adf154149d6e767e21057e0e577b947dd3f66190b212528e1db31", size = 12691517 }, - { url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046 }, - { url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220 }, - { url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377 }, - { url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233 }, + { url = "https://files.pythonhosted.org/packages/7a/08/c008711d1b92ff1272f4fea0fbee57723171f161d42e5c680625535280af/onnxruntime-1.22.0-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:8d6725c5b9a681d8fe72f2960c191a96c256367887d076b08466f52b4e0991df", size = 34282151, upload-time = "2025-05-09T20:25:59.246Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8b/22989f6b59bc4ad1324f07a945c80b9ab825f0a581ad7a6064b93716d9b7/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef17d665a917866d1f68f09edc98223b9a27e6cb167dec69da4c66484ad12fd", size = 14446302, upload-time = "2025-05-09T20:25:44.299Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d5/aa83d084d05bc8f6cf8b74b499c77431ffd6b7075c761ec48ec0c161a47f/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b978aa63a9a22095479c38371a9b359d4c15173cbb164eaad5f2cd27d666aa65", size = 16393496, upload-time = "2025-05-09T20:26:11.588Z" }, + { url = "https://files.pythonhosted.org/packages/89/a5/1c6c10322201566015183b52ef011dfa932f5dd1b278de8d75c3b948411d/onnxruntime-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:03d3ef7fb11adf154149d6e767e21057e0e577b947dd3f66190b212528e1db31", size = 12691517, upload-time = "2025-05-12T21:26:13.354Z" }, + { url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046, upload-time = "2025-05-09T20:26:02.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220, upload-time = "2025-05-09T20:25:47.078Z" }, + { url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377, upload-time = "2025-05-09T20:26:14.478Z" }, + { url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233, upload-time = "2025-05-12T21:26:16.963Z" }, ] [[package]] @@ -3399,25 +3416,48 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/cf/61e71ce64cf0a38f029da0f9a5f10c9fa0e69a7a977b537126dac50adfea/openai-1.61.1.tar.gz", hash = "sha256:ce1851507218209961f89f3520e06726c0aa7d0512386f0f977e3ac3e4f2472e", size = 350784 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/cf/61e71ce64cf0a38f029da0f9a5f10c9fa0e69a7a977b537126dac50adfea/openai-1.61.1.tar.gz", hash = "sha256:ce1851507218209961f89f3520e06726c0aa7d0512386f0f977e3ac3e4f2472e", size = 350784, upload-time = "2025-02-05T14:34:15.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/b6/2e2a011b2dc27a6711376808b4cd8c922c476ea0f1420b39892117fa8563/openai-1.61.1-py3-none-any.whl", hash = "sha256:72b0826240ce26026ac2cd17951691f046e5be82ad122d20a8e1b30ca18bd11e", size = 463126 }, + { url = "https://files.pythonhosted.org/packages/9a/b6/2e2a011b2dc27a6711376808b4cd8c922c476ea0f1420b39892117fa8563/openai-1.61.1-py3-none-any.whl", hash = "sha256:72b0826240ce26026ac2cd17951691f046e5be82ad122d20a8e1b30ca18bd11e", size = 463126, upload-time = "2025-02-05T14:34:13.643Z" }, ] [[package]] name = "opendal" version = "0.45.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/3f/927dfe1349ae58b9238b8eafba747af648d660a9425f486dda01a10f0b78/opendal-0.45.20.tar.gz", hash = "sha256:9f6f90d9e9f9d6e9e5a34aa7729169ef34d2f1869ad1e01ddc39b1c0ce0c9405", size = 990267 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/3f/927dfe1349ae58b9238b8eafba747af648d660a9425f486dda01a10f0b78/opendal-0.45.20.tar.gz", hash = "sha256:9f6f90d9e9f9d6e9e5a34aa7729169ef34d2f1869ad1e01ddc39b1c0ce0c9405", size = 990267, upload-time = "2025-05-26T07:02:11.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/77/6427e16b8630f0cc71f4a1b01648ed3264f1e04f1f6d9b5d09e5c6a4dd2f/opendal-0.45.20-cp311-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35acdd8001e4a741532834fdbff3020ffb10b40028bb49fbe93c4f8197d66d8c", size = 26910966, upload-time = "2025-05-26T07:01:24.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1f/83e415334739f1ab4dba55cdd349abf0b66612249055afb422a354b96ac8/opendal-0.45.20-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:629bfe8d384364bced6cbeb01f49b99779fa5151c68048a1869ff645ddcfcb25", size = 13002770, upload-time = "2025-05-26T07:01:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/49/94/c5de6ed54a02d7413636c2ccefa71d8dd09c2ada1cd6ecab202feb1fdeda/opendal-0.45.20-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cc5ac7e441fb93d86d1673112d9fb08580fc3226f864434f4a56a72efec53", size = 14387218, upload-time = "2025-05-26T07:01:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/83/713a1e1de8cbbd69af50e26644bbdeef3c1068b89f442417376fa3c0f591/opendal-0.45.20-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:45a3adae1f473052234fc4054a6f210df3ded9aff10db8d545d0a37eff3b13cc", size = 13424302, upload-time = "2025-05-26T07:01:36.417Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/c9651e753aaf6eb61887ca372a3f9c2ae57dae03c3159d24deaf018c26dc/opendal-0.45.20-cp311-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8947857052c85a4b0e251d50e23f5f68f0cdd9e509e32e614a5e4b2fc7424c4", size = 13622483, upload-time = "2025-05-26T07:01:38.886Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9d/5d8c20c0fc93df5e349e5694167de30afdc54c5755704cc64764a6cbb309/opendal-0.45.20-cp311-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:891d2f9114efeef648973049ed15e56477e8feb9e48b540bd8d6105ea22a253c", size = 13320229, upload-time = "2025-05-26T07:01:41.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/39/05262f748a2085522e0c85f03eab945589313dc9caedc002872c39162776/opendal-0.45.20-cp311-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:539de9b825f6783d6289d88c0c9ac5415daa4d892d761e3540c565bda51e8997", size = 14574280, upload-time = "2025-05-26T07:01:44.413Z" }, + { url = "https://files.pythonhosted.org/packages/74/83/cc7c6de29b0a7585cd445258d174ca204d37729c3874ad08e515b0bf331c/opendal-0.45.20-cp311-abi3-win_amd64.whl", hash = "sha256:145efd56aa33b493d5b652c3e4f5ae5097ab69d38c132d80f108e9f5c1e4d863", size = 14929888, upload-time = "2025-05-26T07:01:46.929Z" }, +] + +[[package]] +name = "openinference-instrumentation" +version = "0.1.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openinference-semantic-conventions" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/18/d074b45b04ba69bd03260d2dc0a034e5d586d8854e957695f40569278136/openinference_instrumentation-0.1.34.tar.gz", hash = "sha256:fa0328e8b92fc3e22e150c46f108794946ce39fe13670aed15f23ba0105f72ab", size = 22373, upload-time = "2025-06-17T16:47:22.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ad/1a0a5c0a755918269f71fbca225fd70759dd79dd5bffc4723e44f0d87240/openinference_instrumentation-0.1.34-py3-none-any.whl", hash = "sha256:0fff1cc6d9b86f3450fc1c88347c51c5467855992b75e7addb85bf09fd048d2d", size = 28137, upload-time = "2025-06-17T16:47:21.658Z" }, +] + +[[package]] +name = "openinference-semantic-conventions" +version = "0.1.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/0f/b794eb009846d4b10af50e205a323ca359f284563ef4d1778f35a80522ac/openinference_semantic_conventions-0.1.21.tar.gz", hash = "sha256:328405b9f79ff72a659c7712b8429c0d7ea68c6a4a1679e3eb44372aa228119b", size = 12534, upload-time = "2025-06-13T05:22:18.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/77/6427e16b8630f0cc71f4a1b01648ed3264f1e04f1f6d9b5d09e5c6a4dd2f/opendal-0.45.20-cp311-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35acdd8001e4a741532834fdbff3020ffb10b40028bb49fbe93c4f8197d66d8c", size = 26910966 }, - { url = "https://files.pythonhosted.org/packages/12/1f/83e415334739f1ab4dba55cdd349abf0b66612249055afb422a354b96ac8/opendal-0.45.20-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:629bfe8d384364bced6cbeb01f49b99779fa5151c68048a1869ff645ddcfcb25", size = 13002770 }, - { url = "https://files.pythonhosted.org/packages/49/94/c5de6ed54a02d7413636c2ccefa71d8dd09c2ada1cd6ecab202feb1fdeda/opendal-0.45.20-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cc5ac7e441fb93d86d1673112d9fb08580fc3226f864434f4a56a72efec53", size = 14387218 }, - { url = "https://files.pythonhosted.org/packages/c6/83/713a1e1de8cbbd69af50e26644bbdeef3c1068b89f442417376fa3c0f591/opendal-0.45.20-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:45a3adae1f473052234fc4054a6f210df3ded9aff10db8d545d0a37eff3b13cc", size = 13424302 }, - { url = "https://files.pythonhosted.org/packages/c7/78/c9651e753aaf6eb61887ca372a3f9c2ae57dae03c3159d24deaf018c26dc/opendal-0.45.20-cp311-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d8947857052c85a4b0e251d50e23f5f68f0cdd9e509e32e614a5e4b2fc7424c4", size = 13622483 }, - { url = "https://files.pythonhosted.org/packages/3c/9d/5d8c20c0fc93df5e349e5694167de30afdc54c5755704cc64764a6cbb309/opendal-0.45.20-cp311-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:891d2f9114efeef648973049ed15e56477e8feb9e48b540bd8d6105ea22a253c", size = 13320229 }, - { url = "https://files.pythonhosted.org/packages/21/39/05262f748a2085522e0c85f03eab945589313dc9caedc002872c39162776/opendal-0.45.20-cp311-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:539de9b825f6783d6289d88c0c9ac5415daa4d892d761e3540c565bda51e8997", size = 14574280 }, - { url = "https://files.pythonhosted.org/packages/74/83/cc7c6de29b0a7585cd445258d174ca204d37729c3874ad08e515b0bf331c/opendal-0.45.20-cp311-abi3-win_amd64.whl", hash = "sha256:145efd56aa33b493d5b652c3e4f5ae5097ab69d38c132d80f108e9f5c1e4d863", size = 14929888 }, + { url = "https://files.pythonhosted.org/packages/6e/4d/092766f8e610f2c513e483c4adc892eea1634945022a73371fe01f621165/openinference_semantic_conventions-0.1.21-py3-none-any.whl", hash = "sha256:acde8282c20da1de900cdc0d6258a793ec3eb8031bfc496bd823dae17d32e326", size = 10167, upload-time = "2025-06-13T05:22:18.118Z" }, ] [[package]] @@ -3427,9 +3467,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "et-xmlfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] [[package]] @@ -3443,9 +3483,9 @@ dependencies = [ { name = "six" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/dc/acb182db6bb0c71f1e6e41c49260e01d68e52a03efb64e44aed3cc7f483f/opensearch-py-2.4.0.tar.gz", hash = "sha256:7eba2b6ed2ddcf33225bfebfba2aee026877838cc39f760ec80f27827308cc4b", size = 182924 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/dc/acb182db6bb0c71f1e6e41c49260e01d68e52a03efb64e44aed3cc7f483f/opensearch-py-2.4.0.tar.gz", hash = "sha256:7eba2b6ed2ddcf33225bfebfba2aee026877838cc39f760ec80f27827308cc4b", size = 182924, upload-time = "2023-11-15T21:41:37.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/98/178aacf07ece7f95d1948352778702898d57c286053813deb20ebb409923/opensearch_py-2.4.0-py2.py3-none-any.whl", hash = "sha256:316077235437c8ceac970232261f3393c65fb92a80f33c5b106f50f1dab24fd9", size = 258405 }, + { url = "https://files.pythonhosted.org/packages/c1/98/178aacf07ece7f95d1948352778702898d57c286053813deb20ebb409923/opensearch_py-2.4.0-py2.py3-none-any.whl", hash = "sha256:316077235437c8ceac970232261f3393c65fb92a80f33c5b106f50f1dab24fd9", size = 258405, upload-time = "2023-11-15T21:41:35.59Z" }, ] [[package]] @@ -3456,9 +3496,9 @@ dependencies = [ { name = "deprecated" }, { name = "importlib-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/83/93114b6de85a98963aec218a51509a52ed3f8de918fe91eb0f7299805c3f/opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342", size = 62693 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/83/93114b6de85a98963aec218a51509a52ed3f8de918fe91eb0f7299805c3f/opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342", size = 62693, upload-time = "2024-08-28T21:35:31.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/1f/737dcdbc9fea2fa96c1b392ae47275165a7c641663fbb08a8d252968eed2/opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", size = 63970 }, + { url = "https://files.pythonhosted.org/packages/fb/1f/737dcdbc9fea2fa96c1b392ae47275165a7c641663fbb08a8d252968eed2/opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", size = 63970, upload-time = "2024-08-28T21:35:00.598Z" }, ] [[package]] @@ -3470,9 +3510,9 @@ dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/09/423e17c439ed24c45110affe84aad886a536b7871a42637d2ad14a179b47/opentelemetry_distro-0.48b0.tar.gz", hash = "sha256:5cb15915780ac4972583286a56683d43bd4ca95371d72f5f3f179c8b0b2ddc91", size = 2556 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/09/423e17c439ed24c45110affe84aad886a536b7871a42637d2ad14a179b47/opentelemetry_distro-0.48b0.tar.gz", hash = "sha256:5cb15915780ac4972583286a56683d43bd4ca95371d72f5f3f179c8b0b2ddc91", size = 2556, upload-time = "2024-08-28T21:27:40.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/cf/fa9a5fe954f1942e03b319ae0e319ebc93d9f984b548bcd9b3f232a1434d/opentelemetry_distro-0.48b0-py3-none-any.whl", hash = "sha256:b2f8fce114325b020769af3b9bf503efb8af07efc190bd1b9deac7843171664a", size = 3321 }, + { url = "https://files.pythonhosted.org/packages/82/cf/fa9a5fe954f1942e03b319ae0e319ebc93d9f984b548bcd9b3f232a1434d/opentelemetry_distro-0.48b0-py3-none-any.whl", hash = "sha256:b2f8fce114325b020769af3b9bf503efb8af07efc190bd1b9deac7843171664a", size = 3321, upload-time = "2024-08-28T21:26:26.584Z" }, ] [[package]] @@ -3483,9 +3523,9 @@ dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/8156cc14e8f4573a3572ee7f30badc7aabd02961a09acc72ab5f2c789ef1/opentelemetry_exporter_otlp-1.27.0.tar.gz", hash = "sha256:4a599459e623868cc95d933c301199c2367e530f089750e115599fccd67cb2a1", size = 6166 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/8156cc14e8f4573a3572ee7f30badc7aabd02961a09acc72ab5f2c789ef1/opentelemetry_exporter_otlp-1.27.0.tar.gz", hash = "sha256:4a599459e623868cc95d933c301199c2367e530f089750e115599fccd67cb2a1", size = 6166, upload-time = "2024-08-28T21:35:33.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/6d/95e1fc2c8d945a734db32e87a5aa7a804f847c1657a21351df9338bd1c9c/opentelemetry_exporter_otlp-1.27.0-py3-none-any.whl", hash = "sha256:7688791cbdd951d71eb6445951d1cfbb7b6b2d7ee5948fac805d404802931145", size = 7001 }, + { url = "https://files.pythonhosted.org/packages/59/6d/95e1fc2c8d945a734db32e87a5aa7a804f847c1657a21351df9338bd1c9c/opentelemetry_exporter_otlp-1.27.0-py3-none-any.whl", hash = "sha256:7688791cbdd951d71eb6445951d1cfbb7b6b2d7ee5948fac805d404802931145", size = 7001, upload-time = "2024-08-28T21:35:04.02Z" }, ] [[package]] @@ -3495,9 +3535,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/2e/7eaf4ba595fb5213cf639c9158dfb64aacb2e4c7d74bfa664af89fa111f4/opentelemetry_exporter_otlp_proto_common-1.27.0.tar.gz", hash = "sha256:159d27cf49f359e3798c4c3eb8da6ef4020e292571bd8c5604a2a573231dd5c8", size = 17860 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/2e/7eaf4ba595fb5213cf639c9158dfb64aacb2e4c7d74bfa664af89fa111f4/opentelemetry_exporter_otlp_proto_common-1.27.0.tar.gz", hash = "sha256:159d27cf49f359e3798c4c3eb8da6ef4020e292571bd8c5604a2a573231dd5c8", size = 17860, upload-time = "2024-08-28T21:35:34.896Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/27/4610ab3d9bb3cde4309b6505f98b3aabca04a26aa480aa18cede23149837/opentelemetry_exporter_otlp_proto_common-1.27.0-py3-none-any.whl", hash = "sha256:675db7fffcb60946f3a5c43e17d1168a3307a94a930ecf8d2ea1f286f3d4f79a", size = 17848 }, + { url = "https://files.pythonhosted.org/packages/41/27/4610ab3d9bb3cde4309b6505f98b3aabca04a26aa480aa18cede23149837/opentelemetry_exporter_otlp_proto_common-1.27.0-py3-none-any.whl", hash = "sha256:675db7fffcb60946f3a5c43e17d1168a3307a94a930ecf8d2ea1f286f3d4f79a", size = 17848, upload-time = "2024-08-28T21:35:05.412Z" }, ] [[package]] @@ -3513,9 +3553,9 @@ dependencies = [ { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/d0/c1e375b292df26e0ffebf194e82cd197e4c26cc298582bda626ce3ce74c5/opentelemetry_exporter_otlp_proto_grpc-1.27.0.tar.gz", hash = "sha256:af6f72f76bcf425dfb5ad11c1a6d6eca2863b91e63575f89bb7b4b55099d968f", size = 26244 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d0/c1e375b292df26e0ffebf194e82cd197e4c26cc298582bda626ce3ce74c5/opentelemetry_exporter_otlp_proto_grpc-1.27.0.tar.gz", hash = "sha256:af6f72f76bcf425dfb5ad11c1a6d6eca2863b91e63575f89bb7b4b55099d968f", size = 26244, upload-time = "2024-08-28T21:35:36.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/80/32217460c2c64c0568cea38410124ff680a9b65f6732867bbf857c4d8626/opentelemetry_exporter_otlp_proto_grpc-1.27.0-py3-none-any.whl", hash = "sha256:56b5bbd5d61aab05e300d9d62a6b3c134827bbd28d0b12f2649c2da368006c9e", size = 18541 }, + { url = "https://files.pythonhosted.org/packages/8d/80/32217460c2c64c0568cea38410124ff680a9b65f6732867bbf857c4d8626/opentelemetry_exporter_otlp_proto_grpc-1.27.0-py3-none-any.whl", hash = "sha256:56b5bbd5d61aab05e300d9d62a6b3c134827bbd28d0b12f2649c2da368006c9e", size = 18541, upload-time = "2024-08-28T21:35:06.493Z" }, ] [[package]] @@ -3531,9 +3571,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/0a/f05c55e8913bf58a033583f2580a0ec31a5f4cf2beacc9e286dcb74d6979/opentelemetry_exporter_otlp_proto_http-1.27.0.tar.gz", hash = "sha256:2103479092d8eb18f61f3fbff084f67cc7f2d4a7d37e75304b8b56c1d09ebef5", size = 15059 } +sdist = { url = "https://files.pythonhosted.org/packages/31/0a/f05c55e8913bf58a033583f2580a0ec31a5f4cf2beacc9e286dcb74d6979/opentelemetry_exporter_otlp_proto_http-1.27.0.tar.gz", hash = "sha256:2103479092d8eb18f61f3fbff084f67cc7f2d4a7d37e75304b8b56c1d09ebef5", size = 15059, upload-time = "2024-08-28T21:35:37.079Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/8d/4755884afc0b1db6000527cac0ca17273063b6142c773ce4ecd307a82e72/opentelemetry_exporter_otlp_proto_http-1.27.0-py3-none-any.whl", hash = "sha256:688027575c9da42e179a69fe17e2d1eba9b14d81de8d13553a21d3114f3b4d75", size = 17203 }, + { url = "https://files.pythonhosted.org/packages/2d/8d/4755884afc0b1db6000527cac0ca17273063b6142c773ce4ecd307a82e72/opentelemetry_exporter_otlp_proto_http-1.27.0-py3-none-any.whl", hash = "sha256:688027575c9da42e179a69fe17e2d1eba9b14d81de8d13553a21d3114f3b4d75", size = 17203, upload-time = "2024-08-28T21:35:08.141Z" }, ] [[package]] @@ -3545,9 +3585,9 @@ dependencies = [ { name = "setuptools" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0e/d9394839af5d55c8feb3b22cd11138b953b49739b20678ca96289e30f904/opentelemetry_instrumentation-0.48b0.tar.gz", hash = "sha256:94929685d906380743a71c3970f76b5f07476eea1834abd5dd9d17abfe23cc35", size = 24724 } +sdist = { url = "https://files.pythonhosted.org/packages/04/0e/d9394839af5d55c8feb3b22cd11138b953b49739b20678ca96289e30f904/opentelemetry_instrumentation-0.48b0.tar.gz", hash = "sha256:94929685d906380743a71c3970f76b5f07476eea1834abd5dd9d17abfe23cc35", size = 24724, upload-time = "2024-08-28T21:27:42.82Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7f/405c41d4f359121376c9d5117dcf68149b8122d3f6c718996d037bd4d800/opentelemetry_instrumentation-0.48b0-py3-none-any.whl", hash = "sha256:a69750dc4ba6a5c3eb67986a337185a25b739966d80479befe37b546fc870b44", size = 29449 }, + { url = "https://files.pythonhosted.org/packages/0a/7f/405c41d4f359121376c9d5117dcf68149b8122d3f6c718996d037bd4d800/opentelemetry_instrumentation-0.48b0-py3-none-any.whl", hash = "sha256:a69750dc4ba6a5c3eb67986a337185a25b739966d80479befe37b546fc870b44", size = 29449, upload-time = "2024-08-28T21:26:31.288Z" }, ] [[package]] @@ -3561,9 +3601,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/ac/fd3d40bab3234ec3f5c052a815100676baaae1832fa1067935f11e5c59c6/opentelemetry_instrumentation_asgi-0.48b0.tar.gz", hash = "sha256:04c32174b23c7fa72ddfe192dad874954968a6a924608079af9952964ecdf785", size = 23435 } +sdist = { url = "https://files.pythonhosted.org/packages/44/ac/fd3d40bab3234ec3f5c052a815100676baaae1832fa1067935f11e5c59c6/opentelemetry_instrumentation_asgi-0.48b0.tar.gz", hash = "sha256:04c32174b23c7fa72ddfe192dad874954968a6a924608079af9952964ecdf785", size = 23435, upload-time = "2024-08-28T21:27:47.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/74/a0e0d38622856597dd8e630f2bd793760485eb165708e11b8be1696bbb5a/opentelemetry_instrumentation_asgi-0.48b0-py3-none-any.whl", hash = "sha256:ddb1b5fc800ae66e85a4e2eca4d9ecd66367a8c7b556169d9e7b57e10676e44d", size = 15958 }, + { url = "https://files.pythonhosted.org/packages/db/74/a0e0d38622856597dd8e630f2bd793760485eb165708e11b8be1696bbb5a/opentelemetry_instrumentation_asgi-0.48b0-py3-none-any.whl", hash = "sha256:ddb1b5fc800ae66e85a4e2eca4d9ecd66367a8c7b556169d9e7b57e10676e44d", size = 15958, upload-time = "2024-08-28T21:26:38.139Z" }, ] [[package]] @@ -3575,9 +3615,9 @@ dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/68/72975eff50cc22d8f65f96c425a2e8844f91488e78ffcfb603ac7cee0e5a/opentelemetry_instrumentation_celery-0.48b0.tar.gz", hash = "sha256:1d33aa6c4a1e6c5d17a64215245208a96e56c9d07611685dbae09a557704af26", size = 14445 } +sdist = { url = "https://files.pythonhosted.org/packages/42/68/72975eff50cc22d8f65f96c425a2e8844f91488e78ffcfb603ac7cee0e5a/opentelemetry_instrumentation_celery-0.48b0.tar.gz", hash = "sha256:1d33aa6c4a1e6c5d17a64215245208a96e56c9d07611685dbae09a557704af26", size = 14445, upload-time = "2024-08-28T21:27:56.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/59/f09e8f9f596d375fd86b7677751525bbc485c8cc8c5388e39786a3d3b968/opentelemetry_instrumentation_celery-0.48b0-py3-none-any.whl", hash = "sha256:c1904e38cc58fb2a33cd657d6e296285c5ffb0dca3f164762f94b905e5abc88e", size = 13697 }, + { url = "https://files.pythonhosted.org/packages/28/59/f09e8f9f596d375fd86b7677751525bbc485c8cc8c5388e39786a3d3b968/opentelemetry_instrumentation_celery-0.48b0-py3-none-any.whl", hash = "sha256:c1904e38cc58fb2a33cd657d6e296285c5ffb0dca3f164762f94b905e5abc88e", size = 13697, upload-time = "2024-08-28T21:26:50.01Z" }, ] [[package]] @@ -3591,9 +3631,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/20/43477da5850ef2cd3792715d442aecd051e885e0603b6ee5783b2104ba8f/opentelemetry_instrumentation_fastapi-0.48b0.tar.gz", hash = "sha256:21a72563ea412c0b535815aeed75fc580240f1f02ebc72381cfab672648637a2", size = 18497 } +sdist = { url = "https://files.pythonhosted.org/packages/58/20/43477da5850ef2cd3792715d442aecd051e885e0603b6ee5783b2104ba8f/opentelemetry_instrumentation_fastapi-0.48b0.tar.gz", hash = "sha256:21a72563ea412c0b535815aeed75fc580240f1f02ebc72381cfab672648637a2", size = 18497, upload-time = "2024-08-28T21:28:01.14Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/50/745ab075a3041b7a5f29a579d2c28eaad54f64b4589d8f9fd364c62cf0f3/opentelemetry_instrumentation_fastapi-0.48b0-py3-none-any.whl", hash = "sha256:afeb820a59e139d3e5d96619600f11ce0187658b8ae9e3480857dd790bc024f2", size = 11777 }, + { url = "https://files.pythonhosted.org/packages/ee/50/745ab075a3041b7a5f29a579d2c28eaad54f64b4589d8f9fd364c62cf0f3/opentelemetry_instrumentation_fastapi-0.48b0-py3-none-any.whl", hash = "sha256:afeb820a59e139d3e5d96619600f11ce0187658b8ae9e3480857dd790bc024f2", size = 11777, upload-time = "2024-08-28T21:26:57.457Z" }, ] [[package]] @@ -3609,9 +3649,9 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/2f/5c3af780a69f9ba78445fe0e5035c41f67281a31b08f3c3e7ec460bda726/opentelemetry_instrumentation_flask-0.48b0.tar.gz", hash = "sha256:e03a34428071aebf4864ea6c6a564acef64f88c13eb3818e64ea90da61266c3d", size = 19196 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/2f/5c3af780a69f9ba78445fe0e5035c41f67281a31b08f3c3e7ec460bda726/opentelemetry_instrumentation_flask-0.48b0.tar.gz", hash = "sha256:e03a34428071aebf4864ea6c6a564acef64f88c13eb3818e64ea90da61266c3d", size = 19196, upload-time = "2024-08-28T21:28:01.986Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3d/fcde4f8f0bf9fa1ee73a12304fa538076fb83fe0a2ae966ab0f0b7da5109/opentelemetry_instrumentation_flask-0.48b0-py3-none-any.whl", hash = "sha256:26b045420b9d76e85493b1c23fcf27517972423480dc6cf78fd6924248ba5808", size = 14588 }, + { url = "https://files.pythonhosted.org/packages/78/3d/fcde4f8f0bf9fa1ee73a12304fa538076fb83fe0a2ae966ab0f0b7da5109/opentelemetry_instrumentation_flask-0.48b0-py3-none-any.whl", hash = "sha256:26b045420b9d76e85493b1c23fcf27517972423480dc6cf78fd6924248ba5808", size = 14588, upload-time = "2024-08-28T21:26:58.504Z" }, ] [[package]] @@ -3625,9 +3665,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/3fcebbca8bd729da50dc2130d8ca869a235aa5483a85ef06c5dc8643476b/opentelemetry_instrumentation_sqlalchemy-0.48b0.tar.gz", hash = "sha256:dbf2d5a755b470e64e5e2762b56f8d56313787e4c7d71a87fe25c33f48eb3493", size = 13194 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/3fcebbca8bd729da50dc2130d8ca869a235aa5483a85ef06c5dc8643476b/opentelemetry_instrumentation_sqlalchemy-0.48b0.tar.gz", hash = "sha256:dbf2d5a755b470e64e5e2762b56f8d56313787e4c7d71a87fe25c33f48eb3493", size = 13194, upload-time = "2024-08-28T21:28:18.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/84/4b6f1e9e9f83a52d966e91963f5a8424edc4a3d5ea32854c96c2d1618284/opentelemetry_instrumentation_sqlalchemy-0.48b0-py3-none-any.whl", hash = "sha256:625848a34aa5770cb4b1dcdbd95afce4307a0230338711101325261d739f391f", size = 13360 }, + { url = "https://files.pythonhosted.org/packages/e1/84/4b6f1e9e9f83a52d966e91963f5a8424edc4a3d5ea32854c96c2d1618284/opentelemetry_instrumentation_sqlalchemy-0.48b0-py3-none-any.whl", hash = "sha256:625848a34aa5770cb4b1dcdbd95afce4307a0230338711101325261d739f391f", size = 13360, upload-time = "2024-08-28T21:27:22.102Z" }, ] [[package]] @@ -3640,9 +3680,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/a5/f45cdfba18f22aefd2378eac8c07c1f8c9656d6bf7ce315ced48c67f3437/opentelemetry_instrumentation_wsgi-0.48b0.tar.gz", hash = "sha256:1a1e752367b0df4397e0b835839225ef5c2c3c053743a261551af13434fc4d51", size = 17974 } +sdist = { url = "https://files.pythonhosted.org/packages/de/a5/f45cdfba18f22aefd2378eac8c07c1f8c9656d6bf7ce315ced48c67f3437/opentelemetry_instrumentation_wsgi-0.48b0.tar.gz", hash = "sha256:1a1e752367b0df4397e0b835839225ef5c2c3c053743a261551af13434fc4d51", size = 17974, upload-time = "2024-08-28T21:28:24.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/87/fa420007e0ba7e8cd43799ab204717ab515f000236fa2726a6be3299efdd/opentelemetry_instrumentation_wsgi-0.48b0-py3-none-any.whl", hash = "sha256:c6051124d741972090fe94b2fa302555e1e2a22e9cdda32dd39ed49a5b34e0c6", size = 13691 }, + { url = "https://files.pythonhosted.org/packages/fb/87/fa420007e0ba7e8cd43799ab204717ab515f000236fa2726a6be3299efdd/opentelemetry_instrumentation_wsgi-0.48b0-py3-none-any.whl", hash = "sha256:c6051124d741972090fe94b2fa302555e1e2a22e9cdda32dd39ed49a5b34e0c6", size = 13691, upload-time = "2024-08-28T21:27:33.257Z" }, ] [[package]] @@ -3653,9 +3693,9 @@ dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/3ceeb5ff5a1906371834d5c594e24e5b84f35528d219054833deca4ac44c/opentelemetry_propagator_b3-1.27.0.tar.gz", hash = "sha256:39377b6aa619234e08fbc6db79bf880aff36d7e2761efa9afa28b78d5937308f", size = 9590 } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/3ceeb5ff5a1906371834d5c594e24e5b84f35528d219054833deca4ac44c/opentelemetry_propagator_b3-1.27.0.tar.gz", hash = "sha256:39377b6aa619234e08fbc6db79bf880aff36d7e2761efa9afa28b78d5937308f", size = 9590, upload-time = "2024-08-28T21:35:43.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/3f/75ba77b8d9938bae575bc457a5c56ca2246ff5367b54c7d4252a31d1c91f/opentelemetry_propagator_b3-1.27.0-py3-none-any.whl", hash = "sha256:1dd75e9801ba02e870df3830097d35771a64c123127c984d9b05c352a35aa9cc", size = 8899 }, + { url = "https://files.pythonhosted.org/packages/03/3f/75ba77b8d9938bae575bc457a5c56ca2246ff5367b54c7d4252a31d1c91f/opentelemetry_propagator_b3-1.27.0-py3-none-any.whl", hash = "sha256:1dd75e9801ba02e870df3830097d35771a64c123127c984d9b05c352a35aa9cc", size = 8899, upload-time = "2024-08-28T21:35:18.317Z" }, ] [[package]] @@ -3665,9 +3705,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/59/959f0beea798ae0ee9c979b90f220736fbec924eedbefc60ca581232e659/opentelemetry_proto-1.27.0.tar.gz", hash = "sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6", size = 34749 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/59/959f0beea798ae0ee9c979b90f220736fbec924eedbefc60ca581232e659/opentelemetry_proto-1.27.0.tar.gz", hash = "sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6", size = 34749, upload-time = "2024-08-28T21:35:45.839Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/56/3d2d826834209b19a5141eed717f7922150224d1a982385d19a9444cbf8d/opentelemetry_proto-1.27.0-py3-none-any.whl", hash = "sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace", size = 52464 }, + { url = "https://files.pythonhosted.org/packages/94/56/3d2d826834209b19a5141eed717f7922150224d1a982385d19a9444cbf8d/opentelemetry_proto-1.27.0-py3-none-any.whl", hash = "sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace", size = 52464, upload-time = "2024-08-28T21:35:21.434Z" }, ] [[package]] @@ -3679,9 +3719,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/9a/82a6ac0f06590f3d72241a587cb8b0b751bd98728e896cc4cbd4847248e6/opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f", size = 145019 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/9a/82a6ac0f06590f3d72241a587cb8b0b751bd98728e896cc4cbd4847248e6/opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f", size = 145019, upload-time = "2024-08-28T21:35:46.708Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/bd/a6602e71e315055d63b2ff07172bd2d012b4cba2d4e00735d74ba42fc4d6/opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d", size = 110505 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/a6602e71e315055d63b2ff07172bd2d012b4cba2d4e00735d74ba42fc4d6/opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d", size = 110505, upload-time = "2024-08-28T21:35:24.769Z" }, ] [[package]] @@ -3692,44 +3732,56 @@ dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/89/1724ad69f7411772446067cdfa73b598694c8c91f7f8c922e344d96d81f9/opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a", size = 89445 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/89/1724ad69f7411772446067cdfa73b598694c8c91f7f8c922e344d96d81f9/opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a", size = 89445, upload-time = "2024-08-28T21:35:47.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/7a/4f0063dbb0b6c971568291a8bc19a4ca70d3c185db2d956230dd67429dfc/opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f", size = 149685 }, + { url = "https://files.pythonhosted.org/packages/b7/7a/4f0063dbb0b6c971568291a8bc19a4ca70d3c185db2d956230dd67429dfc/opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f", size = 149685, upload-time = "2024-08-28T21:35:25.983Z" }, ] [[package]] name = "opentelemetry-util-http" version = "0.48b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/d7/185c494754340e0a3928fd39fde2616ee78f2c9d66253affaad62d5b7935/opentelemetry_util_http-0.48b0.tar.gz", hash = "sha256:60312015153580cc20f322e5cdc3d3ecad80a71743235bdb77716e742814623c", size = 7863 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/d7/185c494754340e0a3928fd39fde2616ee78f2c9d66253affaad62d5b7935/opentelemetry_util_http-0.48b0.tar.gz", hash = "sha256:60312015153580cc20f322e5cdc3d3ecad80a71743235bdb77716e742814623c", size = 7863, upload-time = "2024-08-28T21:28:27.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/2e/36097c0a4d0115b8c7e377c90bab7783ac183bc5cb4071308f8959454311/opentelemetry_util_http-0.48b0-py3-none-any.whl", hash = "sha256:76f598af93aab50328d2a69c786beaedc8b6a7770f7a818cc307eb353debfffb", size = 6946 }, + { url = "https://files.pythonhosted.org/packages/ad/2e/36097c0a4d0115b8c7e377c90bab7783ac183bc5cb4071308f8959454311/opentelemetry_util_http-0.48b0-py3-none-any.whl", hash = "sha256:76f598af93aab50328d2a69c786beaedc8b6a7770f7a818cc307eb353debfffb", size = 6946, upload-time = "2024-08-28T21:27:37.975Z" }, ] [[package]] name = "opik" -version = "1.7.29" +version = "1.7.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3-stubs", extra = ["bedrock-runtime"] }, { name = "click" }, { name = "httpx" }, { name = "jinja2" }, - { name = "levenshtein" }, { name = "litellm" }, { name = "openai" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pytest" }, + { name = "rapidfuzz" }, { name = "rich" }, { name = "sentry-sdk" }, { name = "tenacity" }, { name = "tqdm" }, { name = "uuid6" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/94/52faf9277891bfa77dc4bac80b5d88d78103d8fad7f6a6c9495ae083e4da/opik-1.7.29.tar.gz", hash = "sha256:5a13692b233d90663a32cbab452937bf8b6fdc2d516c671c102c3beb34301b64", size = 300856 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/52/cea0317bc3207bc967b48932781995d9cdb2c490e7e05caa00ff660f7205/opik-1.7.43.tar.gz", hash = "sha256:0b02522b0b74d0a67b141939deda01f8bb69690eda6b04a7cecb1c7f0649ccd0", size = 326886, upload-time = "2025-07-07T10:30:07.715Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/43/462b700030c70f525eebf4bc63a226ae46109eca598cd59db081de9a8b21/opik-1.7.29-py3-none-any.whl", hash = "sha256:3eb893e7b612d0b799682060193e4f2eebf55b65811203c4fe46af04cf84fa8c", size = 566635 }, + { url = "https://files.pythonhosted.org/packages/76/ae/f3566bdc3c49a1a8f795b1b6e726ef211c87e31f92d870ca6d63999c9bbf/opik-1.7.43-py3-none-any.whl", hash = "sha256:a66395c8b5ea7c24846f72dafc70c74d5b8f24ffbc4c8a1b3a7f9456e550568d", size = 625356, upload-time = "2025-07-07T10:30:06.389Z" }, +] + +[[package]] +name = "optype" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/11/5bc1ad8e4dd339783daec5299c9162eaa80ad072aaa1256561b336152981/optype-0.10.0.tar.gz", hash = "sha256:2b89a1b8b48f9d6dd8c4dd4f59e22557185c81823c6e2bfc43c4819776d5a7ca", size = 95630, upload-time = "2025-05-28T22:43:18.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/98/7f97864d5b6801bc63c24e72c45a58417c344c563ca58134a43249ce8afa/optype-0.10.0-py3-none-any.whl", hash = "sha256:7e9ccc329fb65c326c6bd62c30c2ba03b694c28c378a96c2bcdd18a084f2c96b", size = 83825, upload-time = "2025-05-28T22:43:16.772Z" }, ] [[package]] @@ -3739,56 +3791,56 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/39/712f797b75705c21148fa1d98651f63c2e5cc6876e509a0a9e2f5b406572/oracledb-3.0.0.tar.gz", hash = "sha256:64dc86ee5c032febc556798b06e7b000ef6828bb0252084f6addacad3363db85", size = 840431 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/39/712f797b75705c21148fa1d98651f63c2e5cc6876e509a0a9e2f5b406572/oracledb-3.0.0.tar.gz", hash = "sha256:64dc86ee5c032febc556798b06e7b000ef6828bb0252084f6addacad3363db85", size = 840431, upload-time = "2025-03-03T19:36:12.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/bf/d872c4b3fc15cd3261fe0ea72b21d181700c92dbc050160e161654987062/oracledb-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:52daa9141c63dfa75c07d445e9bb7f69f43bfb3c5a173ecc48c798fe50288d26", size = 4312963 }, - { url = "https://files.pythonhosted.org/packages/b1/ea/01ee29e76a610a53bb34fdc1030f04b7669c3f80b25f661e07850fc6160e/oracledb-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af98941789df4c6aaaf4338f5b5f6b7f2c8c3fe6f8d6a9382f177f350868747a", size = 2661536 }, - { url = "https://files.pythonhosted.org/packages/3d/8e/ad380e34a46819224423b4773e58c350bc6269643c8969604097ced8c3bc/oracledb-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9812bb48865aaec35d73af54cd1746679f2a8a13cbd1412ab371aba2e39b3943", size = 2867461 }, - { url = "https://files.pythonhosted.org/packages/96/09/ecc4384a27fd6e1e4de824ae9c160e4ad3aaebdaade5b4bdcf56a4d1ff63/oracledb-3.0.0-cp311-cp311-win32.whl", hash = "sha256:6c27fe0de64f2652e949eb05b3baa94df9b981a4a45fa7f8a991e1afb450c8e2", size = 1752046 }, - { url = "https://files.pythonhosted.org/packages/62/e8/f34bde24050c6e55eeba46b23b2291f2dd7fd272fa8b322dcbe71be55778/oracledb-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f922709672002f0b40997456f03a95f03e5712a86c61159951c5ce09334325e0", size = 2101210 }, - { url = "https://files.pythonhosted.org/packages/6f/fc/24590c3a3d41e58494bd3c3b447a62835138e5f9b243d9f8da0cfb5da8dc/oracledb-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:acd0e747227dea01bebe627b07e958bf36588a337539f24db629dc3431d3f7eb", size = 4351993 }, - { url = "https://files.pythonhosted.org/packages/b7/b6/1f3b0b7bb94d53e8857d77b2e8dbdf6da091dd7e377523e24b79dac4fd71/oracledb-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8b402f77c22af031cd0051aea2472ecd0635c1b452998f511aa08b7350c90a4", size = 2532640 }, - { url = "https://files.pythonhosted.org/packages/72/1a/1815f6c086ab49c00921cf155ff5eede5267fb29fcec37cb246339a5ce4d/oracledb-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:378a27782e9a37918bd07a5a1427a77cb6f777d0a5a8eac9c070d786f50120ef", size = 2765949 }, - { url = "https://files.pythonhosted.org/packages/33/8d/208900f8d372909792ee70b2daad3f7361181e55f2217c45ed9dff658b54/oracledb-3.0.0-cp312-cp312-win32.whl", hash = "sha256:54a28c2cb08316a527cd1467740a63771cc1c1164697c932aa834c0967dc4efc", size = 1709373 }, - { url = "https://files.pythonhosted.org/packages/0c/5e/c21754f19c896102793c3afec2277e2180aa7d505e4d7fcca24b52d14e4f/oracledb-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8289bad6d103ce42b140e40576cf0c81633e344d56e2d738b539341eacf65624", size = 2056452 }, + { url = "https://files.pythonhosted.org/packages/fa/bf/d872c4b3fc15cd3261fe0ea72b21d181700c92dbc050160e161654987062/oracledb-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:52daa9141c63dfa75c07d445e9bb7f69f43bfb3c5a173ecc48c798fe50288d26", size = 4312963, upload-time = "2025-03-03T19:36:32.576Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ea/01ee29e76a610a53bb34fdc1030f04b7669c3f80b25f661e07850fc6160e/oracledb-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af98941789df4c6aaaf4338f5b5f6b7f2c8c3fe6f8d6a9382f177f350868747a", size = 2661536, upload-time = "2025-03-03T19:36:34.904Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8e/ad380e34a46819224423b4773e58c350bc6269643c8969604097ced8c3bc/oracledb-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9812bb48865aaec35d73af54cd1746679f2a8a13cbd1412ab371aba2e39b3943", size = 2867461, upload-time = "2025-03-03T19:36:36.508Z" }, + { url = "https://files.pythonhosted.org/packages/96/09/ecc4384a27fd6e1e4de824ae9c160e4ad3aaebdaade5b4bdcf56a4d1ff63/oracledb-3.0.0-cp311-cp311-win32.whl", hash = "sha256:6c27fe0de64f2652e949eb05b3baa94df9b981a4a45fa7f8a991e1afb450c8e2", size = 1752046, upload-time = "2025-03-03T19:36:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/62/e8/f34bde24050c6e55eeba46b23b2291f2dd7fd272fa8b322dcbe71be55778/oracledb-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f922709672002f0b40997456f03a95f03e5712a86c61159951c5ce09334325e0", size = 2101210, upload-time = "2025-03-03T19:36:40.669Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fc/24590c3a3d41e58494bd3c3b447a62835138e5f9b243d9f8da0cfb5da8dc/oracledb-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:acd0e747227dea01bebe627b07e958bf36588a337539f24db629dc3431d3f7eb", size = 4351993, upload-time = "2025-03-03T19:36:42.577Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b6/1f3b0b7bb94d53e8857d77b2e8dbdf6da091dd7e377523e24b79dac4fd71/oracledb-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8b402f77c22af031cd0051aea2472ecd0635c1b452998f511aa08b7350c90a4", size = 2532640, upload-time = "2025-03-03T19:36:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/72/1a/1815f6c086ab49c00921cf155ff5eede5267fb29fcec37cb246339a5ce4d/oracledb-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:378a27782e9a37918bd07a5a1427a77cb6f777d0a5a8eac9c070d786f50120ef", size = 2765949, upload-time = "2025-03-03T19:36:47.47Z" }, + { url = "https://files.pythonhosted.org/packages/33/8d/208900f8d372909792ee70b2daad3f7361181e55f2217c45ed9dff658b54/oracledb-3.0.0-cp312-cp312-win32.whl", hash = "sha256:54a28c2cb08316a527cd1467740a63771cc1c1164697c932aa834c0967dc4efc", size = 1709373, upload-time = "2025-03-03T19:36:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5e/c21754f19c896102793c3afec2277e2180aa7d505e4d7fcca24b52d14e4f/oracledb-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8289bad6d103ce42b140e40576cf0c81633e344d56e2d738b539341eacf65624", size = 2056452, upload-time = "2025-03-03T19:36:51.363Z" }, ] [[package]] name = "orjson" version = "3.10.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 }, - { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 }, - { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 }, - { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 }, - { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 }, - { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 }, - { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 }, - { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 }, - { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 }, - { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 }, - { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 }, - { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 }, - { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 }, - { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 }, - { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 }, - { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 }, - { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 }, - { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 }, - { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 }, - { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 }, - { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 }, - { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 }, - { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 }, - { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 }, - { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 }, - { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 }, - { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 }, - { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 }, - { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 }, - { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 }, +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929, upload-time = "2025-04-29T23:28:30.716Z" }, + { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364, upload-time = "2025-04-29T23:28:32.392Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995, upload-time = "2025-04-29T23:28:34.024Z" }, + { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894, upload-time = "2025-04-29T23:28:35.318Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016, upload-time = "2025-04-29T23:28:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290, upload-time = "2025-04-29T23:28:38.3Z" }, + { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829, upload-time = "2025-04-29T23:28:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805, upload-time = "2025-04-29T23:28:40.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008, upload-time = "2025-04-29T23:28:42.284Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419, upload-time = "2025-04-29T23:28:43.673Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292, upload-time = "2025-04-29T23:28:45.573Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182, upload-time = "2025-04-29T23:28:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695, upload-time = "2025-04-29T23:28:48.564Z" }, + { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603, upload-time = "2025-04-29T23:28:50.442Z" }, + { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400, upload-time = "2025-04-29T23:28:51.838Z" }, + { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184, upload-time = "2025-04-29T23:28:53.612Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279, upload-time = "2025-04-29T23:28:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799, upload-time = "2025-04-29T23:28:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791, upload-time = "2025-04-29T23:28:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059, upload-time = "2025-04-29T23:29:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359, upload-time = "2025-04-29T23:29:01.704Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853, upload-time = "2025-04-29T23:29:03.576Z" }, + { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131, upload-time = "2025-04-29T23:29:05.753Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834, upload-time = "2025-04-29T23:29:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368, upload-time = "2025-04-29T23:29:09.301Z" }, + { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359, upload-time = "2025-04-29T23:29:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466, upload-time = "2025-04-29T23:29:12.26Z" }, + { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload-time = "2025-04-29T23:29:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload-time = "2025-04-29T23:29:15.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload-time = "2025-04-29T23:29:17.324Z" }, ] [[package]] @@ -3803,24 +3855,24 @@ dependencies = [ { name = "requests" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/ce/d23a9d44268dc992ae1a878d24341dddaea4de4ae374c261209bb6e9554b/oss2-2.18.5.tar.gz", hash = "sha256:555c857f4441ae42a2c0abab8fc9482543fba35d65a4a4be73101c959a2b4011", size = 283388 } +sdist = { url = "https://files.pythonhosted.org/packages/61/ce/d23a9d44268dc992ae1a878d24341dddaea4de4ae374c261209bb6e9554b/oss2-2.18.5.tar.gz", hash = "sha256:555c857f4441ae42a2c0abab8fc9482543fba35d65a4a4be73101c959a2b4011", size = 283388, upload-time = "2024-04-29T12:49:07.686Z" } [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] [[package]] @@ -3833,22 +3885,22 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, - { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, - { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, - { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, - { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, - { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, - { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, - { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, - { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, - { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, - { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, - { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, - { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, - { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, ] [package.optional-dependencies] @@ -3878,9 +3930,9 @@ dependencies = [ { name = "numpy" }, { name = "types-pytz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/0d/5fe7f7f3596eb1c2526fea151e9470f86b379183d8b9debe44b2098651ca/pandas_stubs-2.2.3.250527.tar.gz", hash = "sha256:e2d694c4e72106055295ad143664e5c99e5815b07190d1ff85b73b13ff019e63", size = 106312 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/0d/5fe7f7f3596eb1c2526fea151e9470f86b379183d8b9debe44b2098651ca/pandas_stubs-2.2.3.250527.tar.gz", hash = "sha256:e2d694c4e72106055295ad143664e5c99e5815b07190d1ff85b73b13ff019e63", size = 106312, upload-time = "2025-05-27T15:24:29.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f8/46141ba8c9d7064dc5008bfb4a6ae5bd3c30e4c61c28b5c5ed485bf358ba/pandas_stubs-2.2.3.250527-py3-none-any.whl", hash = "sha256:cd0a49a95b8c5f944e605be711042a4dd8550e2c559b43d70ba2c4b524b66163", size = 159683 }, + { url = "https://files.pythonhosted.org/packages/ec/f8/46141ba8c9d7064dc5008bfb4a6ae5bd3c30e4c61c28b5c5ed485bf358ba/pandas_stubs-2.2.3.250527-py3-none-any.whl", hash = "sha256:cd0a49a95b8c5f944e605be711042a4dd8550e2c559b43d70ba2c4b524b66163", size = 159683, upload-time = "2025-05-27T15:24:28.4Z" }, ] [[package]] @@ -3891,7 +3943,16 @@ dependencies = [ { name = "plumbum" }, { name = "ply" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/9a/e3186e760c57ee5f1c27ea5cea577a0ff9abfca51eefcb4d9a4cd39aff2e/pandoc-2.4.tar.gz", hash = "sha256:ecd1f8cbb7f4180c6b5db4a17a7c1a74df519995f5f186ef81ce72a9cbd0dd9a", size = 34635 } +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/e3186e760c57ee5f1c27ea5cea577a0ff9abfca51eefcb4d9a4cd39aff2e/pandoc-2.4.tar.gz", hash = "sha256:ecd1f8cbb7f4180c6b5db4a17a7c1a74df519995f5f186ef81ce72a9cbd0dd9a", size = 34635, upload-time = "2024-08-07T14:33:58.016Z" } + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] [[package]] name = "pgvecto-rs" @@ -3901,9 +3962,9 @@ dependencies = [ { name = "numpy" }, { name = "toml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/09/c0be8f54386367159fd22495635fba65ac6bbc436a34502bc2849d89f6ab/pgvecto_rs-0.2.2.tar.gz", hash = "sha256:edaa913d1747152b1407cbdf6337d51ac852547b54953ef38997433be3a75a3b", size = 28561 } +sdist = { url = "https://files.pythonhosted.org/packages/01/09/c0be8f54386367159fd22495635fba65ac6bbc436a34502bc2849d89f6ab/pgvecto_rs-0.2.2.tar.gz", hash = "sha256:edaa913d1747152b1407cbdf6337d51ac852547b54953ef38997433be3a75a3b", size = 28561, upload-time = "2024-10-08T02:01:15.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/dc/a39ceb4fe4b72f889228119b91e0ef7fcaaf9ec662ab19acdacb74cd5eaf/pgvecto_rs-0.2.2-py3-none-any.whl", hash = "sha256:5f3f7f806813de408c45dc10a9eb418b986c4d7b7723e8fce9298f2f7d8fbbd5", size = 30779 }, + { url = "https://files.pythonhosted.org/packages/ba/dc/a39ceb4fe4b72f889228119b91e0ef7fcaaf9ec662ab19acdacb74cd5eaf/pgvecto_rs-0.2.2-py3-none-any.whl", hash = "sha256:5f3f7f806813de408c45dc10a9eb418b986c4d7b7723e8fce9298f2f7d8fbbd5", size = 30779, upload-time = "2024-10-08T02:01:14.669Z" }, ] [package.optional-dependencies] @@ -3919,62 +3980,62 @@ dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/29/bb/4686b1090a7c68fa367e981130a074dc6c1236571d914ffa6e05c882b59d/pgvector-0.2.5-py2.py3-none-any.whl", hash = "sha256:5e5e93ec4d3c45ab1fa388729d56c602f6966296e19deee8878928c6d567e41b", size = 9638 }, + { url = "https://files.pythonhosted.org/packages/29/bb/4686b1090a7c68fa367e981130a074dc6c1236571d914ffa6e05c882b59d/pgvector-0.2.5-py2.py3-none-any.whl", hash = "sha256:5e5e93ec4d3c45ab1fa388729d56c602f6966296e19deee8878928c6d567e41b", size = 9638, upload-time = "2024-02-07T19:35:03.8Z" }, ] [[package]] name = "pillow" -version = "11.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 }, - { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 }, - { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 }, - { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 }, - { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 }, - { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 }, - { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 }, - { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 }, - { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 }, - { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 }, - { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 }, - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 }, - { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 }, - { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 }, - { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 }, - { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 }, - { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 }, - { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 }, - { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 }, +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -3984,18 +4045,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083, upload-time = "2024-10-05T05:59:27.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970 }, + { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970, upload-time = "2024-10-05T05:59:25.102Z" }, ] [[package]] name = "ply" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, ] [[package]] @@ -4005,9 +4066,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891, upload-time = "2024-07-13T23:15:34.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/fb/a70a4214956182e0d7a9099ab17d50bfcba1056188e9b14f35b9e2b62a0d/portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf", size = 18423 }, + { url = "https://files.pythonhosted.org/packages/9b/fb/a70a4214956182e0d7a9099ab17d50bfcba1056188e9b14f35b9e2b62a0d/portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf", size = 18423, upload-time = "2024-07-13T23:15:32.602Z" }, ] [[package]] @@ -4019,14 +4080,14 @@ dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/4c/1053e2e2571e7f39eef8506db94dbe0a37630db97055228f8bdc2e53651c/postgrest-0.17.2.tar.gz", hash = "sha256:445cd4e4a191e279492549df0c4e827d32f9d01d0852599bb8a6efb0f07fcf78", size = 14604 } +sdist = { url = "https://files.pythonhosted.org/packages/d4/4c/1053e2e2571e7f39eef8506db94dbe0a37630db97055228f8bdc2e53651c/postgrest-0.17.2.tar.gz", hash = "sha256:445cd4e4a191e279492549df0c4e827d32f9d01d0852599bb8a6efb0f07fcf78", size = 14604, upload-time = "2024-10-18T08:58:39.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/21/3bdf4c51707f50f4a34839bf4431bad53aa603d303ada961dd9e3d943ecc/postgrest-0.17.2-py3-none-any.whl", hash = "sha256:f7c4f448e5a5e2d4c1dcf192edae9d1007c4261e9a6fb5116783a0046846ece2", size = 21669 }, + { url = "https://files.pythonhosted.org/packages/80/21/3bdf4c51707f50f4a34839bf4431bad53aa603d303ada961dd9e3d943ecc/postgrest-0.17.2-py3-none-any.whl", hash = "sha256:f7c4f448e5a5e2d4c1dcf192edae9d1007c4261e9a6fb5116783a0046846ece2", size = 21669, upload-time = "2024-10-18T08:58:38.13Z" }, ] [[package]] name = "posthog" -version = "4.2.0" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -4034,10 +4095,11 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, { name = "six" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/5b/2e9890700b7b55a370edbfbe5948eae780d48af9b46ad06ea2e7970576f4/posthog-4.2.0.tar.gz", hash = "sha256:c4abc95de03294be005b3b7e8735e9d7abab88583da26262112bacce64b0c3b5", size = 80727 } +sdist = { url = "https://files.pythonhosted.org/packages/39/a2/1b68562124b0d0e615fa8431cc88c84b3db6526275c2c19a419579a49277/posthog-6.0.3.tar.gz", hash = "sha256:9005abb341af8fedd9d82ca0359b3d35a9537555cdc9881bfb469f7c0b4b0ec5", size = 91861, upload-time = "2025-07-07T07:14:08.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/16/7b6c5844acee2d343d463ee0e3143cd8c7c48a6c0d079a2f7daf0c80b95c/posthog-4.2.0-py2.py3-none-any.whl", hash = "sha256:60c7066caac43e43e326e9196d8c1aadeafc8b0be9e5c108446e352711fa456b", size = 96692 }, + { url = "https://files.pythonhosted.org/packages/ca/f1/a8d86245d41c8686f7d828a4959bdf483e8ac331b249b48b8c61fc884a1c/posthog-6.0.3-py3-none-any.whl", hash = "sha256:4b808c907f3623216a9362d91fdafce8e2f57a8387fb3020475c62ec809be56d", size = 108978, upload-time = "2025-07-07T07:14:06.451Z" }, ] [[package]] @@ -4047,50 +4109,50 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] [[package]] name = "propcache" -version = "0.3.1" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/0f/5a5319ee83bd651f75311fcb0c492c21322a7fc8f788e4eef23f44243427/propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", size = 80243 }, - { url = "https://files.pythonhosted.org/packages/ce/84/3db5537e0879942783e2256616ff15d870a11d7ac26541336fe1b673c818/propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", size = 46503 }, - { url = "https://files.pythonhosted.org/packages/e2/c8/b649ed972433c3f0d827d7f0cf9ea47162f4ef8f4fe98c5f3641a0bc63ff/propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", size = 45934 }, - { url = "https://files.pythonhosted.org/packages/59/f9/4c0a5cf6974c2c43b1a6810c40d889769cc8f84cea676cbe1e62766a45f8/propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", size = 233633 }, - { url = "https://files.pythonhosted.org/packages/e7/64/66f2f4d1b4f0007c6e9078bd95b609b633d3957fe6dd23eac33ebde4b584/propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", size = 241124 }, - { url = "https://files.pythonhosted.org/packages/aa/bf/7b8c9fd097d511638fa9b6af3d986adbdf567598a567b46338c925144c1b/propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", size = 240283 }, - { url = "https://files.pythonhosted.org/packages/fa/c9/e85aeeeaae83358e2a1ef32d6ff50a483a5d5248bc38510d030a6f4e2816/propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", size = 232498 }, - { url = "https://files.pythonhosted.org/packages/8e/66/acb88e1f30ef5536d785c283af2e62931cb934a56a3ecf39105887aa8905/propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", size = 221486 }, - { url = "https://files.pythonhosted.org/packages/f5/f9/233ddb05ffdcaee4448508ee1d70aa7deff21bb41469ccdfcc339f871427/propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", size = 222675 }, - { url = "https://files.pythonhosted.org/packages/98/b8/eb977e28138f9e22a5a789daf608d36e05ed93093ef12a12441030da800a/propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", size = 215727 }, - { url = "https://files.pythonhosted.org/packages/89/2d/5f52d9c579f67b8ee1edd9ec073c91b23cc5b7ff7951a1e449e04ed8fdf3/propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", size = 217878 }, - { url = "https://files.pythonhosted.org/packages/7a/fd/5283e5ed8a82b00c7a989b99bb6ea173db1ad750bf0bf8dff08d3f4a4e28/propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", size = 230558 }, - { url = "https://files.pythonhosted.org/packages/90/38/ab17d75938ef7ac87332c588857422ae126b1c76253f0f5b1242032923ca/propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", size = 233754 }, - { url = "https://files.pythonhosted.org/packages/06/5d/3b921b9c60659ae464137508d3b4c2b3f52f592ceb1964aa2533b32fcf0b/propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", size = 226088 }, - { url = "https://files.pythonhosted.org/packages/54/6e/30a11f4417d9266b5a464ac5a8c5164ddc9dd153dfa77bf57918165eb4ae/propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", size = 40859 }, - { url = "https://files.pythonhosted.org/packages/1d/3a/8a68dd867da9ca2ee9dfd361093e9cb08cb0f37e5ddb2276f1b5177d7731/propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", size = 45153 }, - { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430 }, - { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637 }, - { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123 }, - { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031 }, - { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100 }, - { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170 }, - { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000 }, - { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262 }, - { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772 }, - { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133 }, - { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741 }, - { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047 }, - { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467 }, - { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022 }, - { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647 }, - { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784 }, - { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 }, +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[package]] @@ -4100,103 +4162,103 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 }, + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, ] [[package]] name = "protobuf" version = "4.25.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920 } +sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745 }, - { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736 }, - { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537 }, - { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005 }, - { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924 }, - { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757 }, + { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] [[package]] name = "psycogreen" version = "1.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/72/4a7965cf54e341006ad74cdc72cd6572c789bc4f4e3fadc78672f1fbcfbd/psycogreen-1.0.2.tar.gz", hash = "sha256:c429845a8a49cf2f76b71265008760bcd7c7c77d80b806db4dc81116dbcd130d", size = 5411 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/72/4a7965cf54e341006ad74cdc72cd6572c789bc4f4e3fadc78672f1fbcfbd/psycogreen-1.0.2.tar.gz", hash = "sha256:c429845a8a49cf2f76b71265008760bcd7c7c77d80b806db4dc81116dbcd130d", size = 5411, upload-time = "2020-02-22T19:55:22.02Z" } [[package]] name = "psycopg2-binary" version = "2.9.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397 }, - { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806 }, - { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370 }, - { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780 }, - { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583 }, - { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831 }, - { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822 }, - { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975 }, - { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320 }, - { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617 }, - { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618 }, - { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816 }, - { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, - { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, - { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, - { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, - { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, - { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, - { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, - { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, - { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, - { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, - { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, - { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397, upload-time = "2024-10-16T11:19:40.033Z" }, + { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806, upload-time = "2024-10-16T11:19:43.5Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370, upload-time = "2024-10-16T11:19:46.986Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780, upload-time = "2024-10-16T11:19:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583, upload-time = "2024-10-16T11:19:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831, upload-time = "2024-10-16T11:19:57.762Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822, upload-time = "2024-10-16T11:20:04.693Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975, upload-time = "2024-10-16T11:20:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320, upload-time = "2024-10-16T11:20:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617, upload-time = "2024-10-16T11:20:24.711Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618, upload-time = "2024-10-16T11:20:27.718Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816, upload-time = "2024-10-16T11:20:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" }, ] [[package]] name = "py" version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716 } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -4206,41 +4268,41 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pycryptodome" version = "3.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/38/42a8855ff1bf568c61ca6557e2203f318fb7afeadaf2eb8ecfdbde107151/pycryptodome-3.19.1.tar.gz", hash = "sha256:8ae0dd1bcfada451c35f9e29a3e5db385caabc190f98e4a80ad02a61098fb776", size = 4782144 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/38/42a8855ff1bf568c61ca6557e2203f318fb7afeadaf2eb8ecfdbde107151/pycryptodome-3.19.1.tar.gz", hash = "sha256:8ae0dd1bcfada451c35f9e29a3e5db385caabc190f98e4a80ad02a61098fb776", size = 4782144, upload-time = "2023-12-28T06:52:40.741Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/ef/4931bc30674f0de0ca0e827b58c8b0c17313a8eae2754976c610b866118b/pycryptodome-3.19.1-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:67939a3adbe637281c611596e44500ff309d547e932c449337649921b17b6297", size = 2417027 }, - { url = "https://files.pythonhosted.org/packages/67/e6/238c53267fd8d223029c0a0d3730cb1b6594d60f62e40c4184703dc490b1/pycryptodome-3.19.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:11ddf6c9b52116b62223b6a9f4741bc4f62bb265392a4463282f7f34bb287180", size = 1579728 }, - { url = "https://files.pythonhosted.org/packages/7c/87/7181c42c8d5ba89822a4b824830506d0aeec02959bb893614767e3279846/pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e6f89480616781d2a7f981472d0cdb09b9da9e8196f43c1234eff45c915766", size = 2051440 }, - { url = "https://files.pythonhosted.org/packages/34/dd/332c4c0055527d17dac317ed9f9c864fc047b627d82f4b9a56c110afc6fc/pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e1efcb68993b7ce5d1d047a46a601d41281bba9f1971e6be4aa27c69ab8065", size = 2125379 }, - { url = "https://files.pythonhosted.org/packages/24/9e/320b885ea336c218ff54ec2b276cd70ba6904e4f5a14a771ed39a2c47d59/pycryptodome-3.19.1-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c6273ca5a03b672e504995529b8bae56da0ebb691d8ef141c4aa68f60765700", size = 2153951 }, - { url = "https://files.pythonhosted.org/packages/f4/54/8ae0c43d1257b41bc9d3277c3f875174fd8ad86b9567f0b8609b99c938ee/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b0bfe61506795877ff974f994397f0c862d037f6f1c0bfc3572195fc00833b96", size = 2044041 }, - { url = "https://files.pythonhosted.org/packages/45/93/f8450a92cc38541c3ba1f4cb4e267e15ae6d6678ca617476d52c3a3764d4/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:f34976c5c8eb79e14c7d970fb097482835be8d410a4220f86260695ede4c3e17", size = 2182446 }, - { url = "https://files.pythonhosted.org/packages/af/cd/ed6e429fb0792ce368f66e83246264dd3a7a045b0b1e63043ed22a063ce5/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7c9e222d0976f68d0cf6409cfea896676ddc1d98485d601e9508f90f60e2b0a2", size = 2144914 }, - { url = "https://files.pythonhosted.org/packages/f6/23/b064bd4cfbf2cc5f25afcde0e7c880df5b20798172793137ba4b62d82e72/pycryptodome-3.19.1-cp35-abi3-win32.whl", hash = "sha256:4805e053571140cb37cf153b5c72cd324bb1e3e837cbe590a19f69b6cf85fd03", size = 1713105 }, - { url = "https://files.pythonhosted.org/packages/7d/e0/ded1968a5257ab34216a0f8db7433897a2337d59e6d03be113713b346ea2/pycryptodome-3.19.1-cp35-abi3-win_amd64.whl", hash = "sha256:a470237ee71a1efd63f9becebc0ad84b88ec28e6784a2047684b693f458f41b7", size = 1749222 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/4931bc30674f0de0ca0e827b58c8b0c17313a8eae2754976c610b866118b/pycryptodome-3.19.1-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:67939a3adbe637281c611596e44500ff309d547e932c449337649921b17b6297", size = 2417027, upload-time = "2023-12-28T06:51:50.138Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/238c53267fd8d223029c0a0d3730cb1b6594d60f62e40c4184703dc490b1/pycryptodome-3.19.1-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:11ddf6c9b52116b62223b6a9f4741bc4f62bb265392a4463282f7f34bb287180", size = 1579728, upload-time = "2023-12-28T06:51:52.385Z" }, + { url = "https://files.pythonhosted.org/packages/7c/87/7181c42c8d5ba89822a4b824830506d0aeec02959bb893614767e3279846/pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e6f89480616781d2a7f981472d0cdb09b9da9e8196f43c1234eff45c915766", size = 2051440, upload-time = "2023-12-28T06:51:55.751Z" }, + { url = "https://files.pythonhosted.org/packages/34/dd/332c4c0055527d17dac317ed9f9c864fc047b627d82f4b9a56c110afc6fc/pycryptodome-3.19.1-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e1efcb68993b7ce5d1d047a46a601d41281bba9f1971e6be4aa27c69ab8065", size = 2125379, upload-time = "2023-12-28T06:51:58.567Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/320b885ea336c218ff54ec2b276cd70ba6904e4f5a14a771ed39a2c47d59/pycryptodome-3.19.1-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c6273ca5a03b672e504995529b8bae56da0ebb691d8ef141c4aa68f60765700", size = 2153951, upload-time = "2023-12-28T06:52:01.699Z" }, + { url = "https://files.pythonhosted.org/packages/f4/54/8ae0c43d1257b41bc9d3277c3f875174fd8ad86b9567f0b8609b99c938ee/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b0bfe61506795877ff974f994397f0c862d037f6f1c0bfc3572195fc00833b96", size = 2044041, upload-time = "2023-12-28T06:52:03.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/f8450a92cc38541c3ba1f4cb4e267e15ae6d6678ca617476d52c3a3764d4/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:f34976c5c8eb79e14c7d970fb097482835be8d410a4220f86260695ede4c3e17", size = 2182446, upload-time = "2023-12-28T06:52:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ed6e429fb0792ce368f66e83246264dd3a7a045b0b1e63043ed22a063ce5/pycryptodome-3.19.1-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7c9e222d0976f68d0cf6409cfea896676ddc1d98485d601e9508f90f60e2b0a2", size = 2144914, upload-time = "2023-12-28T06:52:07.44Z" }, + { url = "https://files.pythonhosted.org/packages/f6/23/b064bd4cfbf2cc5f25afcde0e7c880df5b20798172793137ba4b62d82e72/pycryptodome-3.19.1-cp35-abi3-win32.whl", hash = "sha256:4805e053571140cb37cf153b5c72cd324bb1e3e837cbe590a19f69b6cf85fd03", size = 1713105, upload-time = "2023-12-28T06:52:09.585Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e0/ded1968a5257ab34216a0f8db7433897a2337d59e6d03be113713b346ea2/pycryptodome-3.19.1-cp35-abi3-win_amd64.whl", hash = "sha256:a470237ee71a1efd63f9becebc0ad84b88ec28e6784a2047684b693f458f41b7", size = 1749222, upload-time = "2023-12-28T06:52:11.534Z" }, ] [[package]] name = "pydantic" -version = "2.11.5" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4248,9 +4310,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] [[package]] @@ -4260,45 +4322,45 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] @@ -4309,9 +4371,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ba/4178111ec4116c54e1dc7ecd2a1ff8f54256cdbd250e576882911e8f710a/pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8", size = 138429 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ba/4178111ec4116c54e1dc7ecd2a1ff8f54256cdbd250e576882911e8f710a/pydantic_extra_types-2.10.5.tar.gz", hash = "sha256:1dcfa2c0cf741a422f088e0dbb4690e7bfadaaf050da3d6f80d6c3cf58a2bad8", size = 138429, upload-time = "2025-06-02T09:31:52.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/1a/5f4fd9e7285f10c44095a4f9fe17d0f358d1702a7c74a9278c794e8a7537/pydantic_extra_types-2.10.5-py3-none-any.whl", hash = "sha256:b60c4e23d573a69a4f1a16dd92888ecc0ef34fb0e655b4f305530377fa70e7a8", size = 38315 }, + { url = "https://files.pythonhosted.org/packages/70/1a/5f4fd9e7285f10c44095a4f9fe17d0f358d1702a7c74a9278c794e8a7537/pydantic_extra_types-2.10.5-py3-none-any.whl", hash = "sha256:b60c4e23d573a69a4f1a16dd92888ecc0ef34fb0e655b4f305530377fa70e7a8", size = 38315, upload-time = "2025-06-02T09:31:51.229Z" }, ] [[package]] @@ -4323,27 +4385,27 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", size = 78313 } +sdist = { url = "https://files.pythonhosted.org/packages/30/72/8259b2bccfe4673330cea843ab23f86858a419d8f1493f66d413a76c7e3b/PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", size = 78313, upload-time = "2023-07-18T20:02:22.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", size = 22591 }, + { url = "https://files.pythonhosted.org/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320", size = 22591, upload-time = "2023-07-18T20:02:21.561Z" }, ] [package.optional-dependencies] @@ -4353,7 +4415,7 @@ crypto = [ [[package]] name = "pymilvus" -version = "2.5.10" +version = "2.5.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio" }, @@ -4364,9 +4426,9 @@ dependencies = [ { name = "setuptools" }, { name = "ujson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/e2/88f126a08d8eefba7341e3eb323406a227146094aab7137a2b91d882e98d/pymilvus-2.5.10.tar.gz", hash = "sha256:cc44ad776aeab781ee4c4a4d334b73e746066ab2fb6722c5311f02efa6fc54a2", size = 1260364 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/53/4af820a37163225a76656222ee43a0eb8f1bd2ceec063315680a585435da/pymilvus-2.5.12.tar.gz", hash = "sha256:79ec7dc0616c2484f77abe98bca8deafb613645b5703c492b51961afd4f985d8", size = 1265893, upload-time = "2025-07-02T15:34:00.385Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/4b/847704930ad8ddd0d0975e9a3a5e3fe704f642debe97454135c2b9ee7081/pymilvus-2.5.10-py3-none-any.whl", hash = "sha256:7da540f93068871cda3941602c55227aeaafb66f2f0d9c05e8f9db783716b100", size = 227635 }, + { url = "https://files.pythonhosted.org/packages/68/4f/80a4940f2772d10272c3292444af767a5aa1a5bbb631874568713ca01d54/pymilvus-2.5.12-py3-none-any.whl", hash = "sha256:ef77a4a0076469a30b05f0bb23b5a058acfbdca83d82af9574ca651764017f44", size = 231425, upload-time = "2025-07-02T15:33:58.938Z" }, ] [[package]] @@ -4378,106 +4440,104 @@ dependencies = [ { name = "orjson" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/da/3027eeeaf7a7db9b0ca761079de4e676a002e1cc2c4260dab0ce812972b8/pymochow-1.3.1.tar.gz", hash = "sha256:1693d10cd0bb7bce45327890a90adafb503155922ccc029acb257699a73a20ba", size = 30800 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/da/3027eeeaf7a7db9b0ca761079de4e676a002e1cc2c4260dab0ce812972b8/pymochow-1.3.1.tar.gz", hash = "sha256:1693d10cd0bb7bce45327890a90adafb503155922ccc029acb257699a73a20ba", size = 30800, upload-time = "2024-09-11T12:06:37.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/74/4b6227717f6baa37e7288f53e0fd55764939abc4119342eed4924a98f477/pymochow-1.3.1-py3-none-any.whl", hash = "sha256:a7f3b34fd6ea5d1d8413650bb6678365aa148fc396ae945e4ccb4f2365a52327", size = 42697 }, + { url = "https://files.pythonhosted.org/packages/6b/74/4b6227717f6baa37e7288f53e0fd55764939abc4119342eed4924a98f477/pymochow-1.3.1-py3-none-any.whl", hash = "sha256:a7f3b34fd6ea5d1d8413650bb6678365aa148fc396ae945e4ccb4f2365a52327", size = 42697, upload-time = "2024-09-11T12:06:36.114Z" }, ] [[package]] name = "pymysql" version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678, upload-time = "2024-05-21T11:03:43.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972 }, + { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972, upload-time = "2024-05-21T11:03:41.216Z" }, ] [[package]] name = "pyobvector" -version = "0.1.20" +version = "0.1.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiomysql" }, { name = "numpy" }, - { name = "pydantic" }, { name = "pymysql" }, { name = "sqlalchemy" }, - { name = "sqlglot" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/f7/fcc99e41bac6aee0822667809957800b2f512f2d96d1fa52c283cb58db16/pyobvector-0.1.20.tar.gz", hash = "sha256:26e8155d5b933333a2ab21b08e51df84e374d188f5fb26520347450e72f34c6e", size = 35256 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/59/7d762061808948dd6aad165a000b34e22163dc83fb5014184eeacc0fabe5/pyobvector-0.1.14.tar.gz", hash = "sha256:4f85cdd63064d040e94c0a96099a0cd5cda18ce625865382e89429f28422fc02", size = 26780, upload-time = "2024-11-20T11:46:18.017Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/7d/00cd33c18814bec0a4f6f4a89becdbe2c6c28d78f87d765d0259a22b117e/pyobvector-0.1.20-py3-none-any.whl", hash = "sha256:1a991f8b9f53e1a749fc396ccb90c7591e64b2c5679930023f00789c62e7af72", size = 46264 }, + { url = "https://files.pythonhosted.org/packages/88/68/ecb21b74c974e7be7f9034e205d08db62d614ff5c221581ae96d37ef853e/pyobvector-0.1.14-py3-none-any.whl", hash = "sha256:828e0bec49a177355b70c7a1270af3b0bf5239200ee0d096e4165b267eeff97c", size = 35526, upload-time = "2024-11-20T11:46:16.809Z" }, ] [[package]] name = "pypandoc" version = "1.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/26e650d053df5f3874aa3c05901a14166ce3271f58bfe114fd776987efbd/pypandoc-1.15.tar.gz", hash = "sha256:ea25beebe712ae41d63f7410c08741a3cab0e420f6703f95bc9b3a749192ce13", size = 32940 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/26e650d053df5f3874aa3c05901a14166ce3271f58bfe114fd776987efbd/pypandoc-1.15.tar.gz", hash = "sha256:ea25beebe712ae41d63f7410c08741a3cab0e420f6703f95bc9b3a749192ce13", size = 32940, upload-time = "2025-01-08T17:39:58.705Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/06/0763e0ccc81754d3eadb21b2cb86cf21bdedc9b52698c2ad6785db7f0a4e/pypandoc-1.15-py3-none-any.whl", hash = "sha256:4ededcc76c8770f27aaca6dff47724578428eca84212a31479403a9731fc2b16", size = 21321 }, + { url = "https://files.pythonhosted.org/packages/61/06/0763e0ccc81754d3eadb21b2cb86cf21bdedc9b52698c2ad6785db7f0a4e/pypandoc-1.15-py3-none-any.whl", hash = "sha256:4ededcc76c8770f27aaca6dff47724578428eca84212a31479403a9731fc2b16", size = 21321, upload-time = "2025-01-08T17:39:09.928Z" }, ] [[package]] name = "pyparsing" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, ] [[package]] name = "pypdf" -version = "5.6.0" +version = "5.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/46/67de1d7a65412aa1c896e6b280829b70b57d203fadae6859b690006b8e0a/pypdf-5.6.0.tar.gz", hash = "sha256:a4b6538b77fc796622000db7127e4e58039ec5e6afd292f8e9bf42e2e985a749", size = 5023749 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/42/fbc37af367b20fa6c53da81b1780025f6046a0fac8cbf0663a17e743b033/pypdf-5.7.0.tar.gz", hash = "sha256:68c92f2e1aae878bab1150e74447f31ab3848b1c0a6f8becae9f0b1904460b6f", size = 5026120, upload-time = "2025-06-29T08:49:48.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/8b/dc3a72d98c22be7a4cbd664ad14c5a3e6295c2dbdf572865ed61e24b5e38/pypdf-5.6.0-py3-none-any.whl", hash = "sha256:ca6bf446bfb0a2d8d71d6d6bb860798d864c36a29b3d9ae8d7fc7958c59f88e7", size = 304208 }, + { url = "https://files.pythonhosted.org/packages/73/9f/78d096ef795a813fa0e1cb9b33fa574b205f2b563d9c1e9366c854cf0364/pypdf-5.7.0-py3-none-any.whl", hash = "sha256:203379453439f5b68b7a1cd43cdf4c5f7a02b84810cefa7f93a47b350aaaba48", size = 305524, upload-time = "2025-06-29T08:49:46.16Z" }, ] [[package]] name = "pypdfium2" version = "4.30.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239, upload-time = "2024-05-09T18:33:17.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254 }, - { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624 }, - { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126 }, - { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077 }, - { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431 }, - { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008 }, - { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543 }, - { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911 }, - { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430 }, - { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951 }, - { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098 }, - { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118 }, + { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254, upload-time = "2024-05-09T18:32:48.653Z" }, + { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624, upload-time = "2024-05-09T18:32:51.458Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126, upload-time = "2024-05-09T18:32:53.581Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077, upload-time = "2024-05-09T18:32:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431, upload-time = "2024-05-09T18:32:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008, upload-time = "2024-05-09T18:32:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543, upload-time = "2024-05-09T18:33:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911, upload-time = "2024-05-09T18:33:05.376Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430, upload-time = "2024-05-09T18:33:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951, upload-time = "2024-05-09T18:33:10.567Z" }, + { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098, upload-time = "2024-05-09T18:33:13.107Z" }, + { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118, upload-time = "2024-05-09T18:33:15.489Z" }, ] [[package]] name = "pypika" version = "0.48.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259, upload-time = "2022-03-15T11:22:57.066Z" } [[package]] name = "pyproject-hooks" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pyreadline3" version = "3.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] [[package]] @@ -4490,9 +4550,9 @@ dependencies = [ { name = "packaging" }, { name = "pluggy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -4503,9 +4563,9 @@ dependencies = [ { name = "py-cpuinfo" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641 } +sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641, upload-time = "2022-10-25T21:21:55.686Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/a1/3b70862b5b3f830f0422844f25a823d0470739d994466be9dbbbb414d85a/pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6", size = 43951 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/3b70862b5b3f830f0422844f25a823d0470739d994466be9dbbbb414d85a/pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6", size = 43951, upload-time = "2022-10-25T21:21:53.208Z" }, ] [[package]] @@ -4516,9 +4576,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245 } +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949 }, + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, ] [[package]] @@ -4528,9 +4588,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141 }, + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, ] [[package]] @@ -4540,46 +4600,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241 } +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 }, + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] [[package]] name = "python-calamine" -version = "0.3.2" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/21/387b92059909e741af7837194d84250335d2a057f614752b6364aaaa2f56/python_calamine-0.3.2.tar.gz", hash = "sha256:5cf12f2086373047cdea681711857b672cba77a34a66dd3755d60686fc974e06", size = 117336 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/b7/d59863ebe319150739d0c352c6dea2710a2f90254ed32304d52e8349edce/python_calamine-0.3.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5251746816069c38eafdd1e4eb7b83870e1fe0ff6191ce9a809b187ffba8ce93", size = 830854 }, - { url = "https://files.pythonhosted.org/packages/d3/01/b48c6f2c2e530a1a031199c5c5bf35f7c2cf7f16f3989263e616e3bc86ce/python_calamine-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9775dbc93bc635d48f45433f8869a546cca28c2a86512581a05333f97a18337b", size = 809411 }, - { url = "https://files.pythonhosted.org/packages/fe/6d/69c53ffb11b3ee1bf5bd945cc2514848adea492c879a50f38e2ed4424727/python_calamine-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff4318b72ba78e8a04fb4c45342cfa23eab6f81ecdb85548cdab9f2db8ac9c7", size = 872905 }, - { url = "https://files.pythonhosted.org/packages/be/ec/b02c4bc04c426d153af1f5ff07e797dd81ada6f47c170e0207d07c90b53a/python_calamine-0.3.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0cd8eb1ef8644da71788a33d3de602d1c08ff1c4136942d87e25f09580b512ef", size = 876464 }, - { url = "https://files.pythonhosted.org/packages/46/ef/8403ee595207de5bd277279b56384b31390987df8a61c280b4176802481a/python_calamine-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcfd560d8f88f39d23b829f666ebae4bd8daeec7ed57adfb9313543f3c5fa35", size = 942289 }, - { url = "https://files.pythonhosted.org/packages/89/97/b4e5b77c70b36613c10f2dbeece75b5d43727335a33bf5176792ec83c3fc/python_calamine-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5e79b9eae4b30c82d045f9952314137c7089c88274e1802947f9e3adb778a59", size = 978699 }, - { url = "https://files.pythonhosted.org/packages/5f/e9/03bbafd6b11cdf70c004f2e856978fc252ec5ea7e77529f14f969134c7a8/python_calamine-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5e8cc518c8e3e5988c5c658f9dcd8229f5541ca63353175bb15b6ad8c456d0", size = 886008 }, - { url = "https://files.pythonhosted.org/packages/7b/20/e18f534e49b403ba0b979a4dfead146001d867f5be846b91f81ed5377972/python_calamine-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a0e596b1346c28b2de15c9f86186cceefa4accb8882992aa0b7499c593446ed", size = 925104 }, - { url = "https://files.pythonhosted.org/packages/54/4c/58933e69a0a7871487d10b958c1f83384bc430d53efbbfbf1dea141a0d85/python_calamine-0.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f521de16a9f3e951ec2e5e35d76752fe004088dbac4cdbf4dd62d0ad2bbf650f", size = 1050448 }, - { url = "https://files.pythonhosted.org/packages/83/95/5c96d093eaaa2d15c63b43bcf8c87708eaab8428c72b6ebdcafc2604aa47/python_calamine-0.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:417d6825a36bba526ae17bed1b6ca576fbb54e23dc60c97eeb536c622e77c62f", size = 1056840 }, - { url = "https://files.pythonhosted.org/packages/23/e0/b03cc3ad4f40fd3be0ebac0b71d273864ddf2bf0e611ec309328fdedded9/python_calamine-0.3.2-cp311-cp311-win32.whl", hash = "sha256:cd3ea1ca768139753633f9f0b16997648db5919894579f363d71f914f85f7ade", size = 663268 }, - { url = "https://files.pythonhosted.org/packages/6b/bd/550da64770257fc70a185482f6353c0654a11f381227e146bb0170db040f/python_calamine-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:4560100412d8727c49048cca102eadeb004f91cfb9c99ae63cd7d4dc0a61333a", size = 692393 }, - { url = "https://files.pythonhosted.org/packages/be/2e/0b4b7a146c3bb41116fe8e59a2f616340786db12aed51c7a9e75817cfa03/python_calamine-0.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:a2526e6ba79087b1634f49064800339edb7316780dd7e1e86d10a0ca9de4e90f", size = 667312 }, - { url = "https://files.pythonhosted.org/packages/f2/0f/c2e3e3bae774dae47cba6ffa640ff95525bd6a10a13d3cd998f33aeafc7f/python_calamine-0.3.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7c063b1f783352d6c6792305b2b0123784882e2436b638a9b9a1e97f6d74fa51", size = 825179 }, - { url = "https://files.pythonhosted.org/packages/c7/81/a05285f06d71ea38ab99b09f3119f93f575487c9d24d7a1bab65657b258b/python_calamine-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85016728937e8f5d1810ff3c9603ffd2458d66e34d495202d7759fa8219871cd", size = 804036 }, - { url = "https://files.pythonhosted.org/packages/24/b5/320f366ffd91ee5d5f0f77817d4fb684f62a5a68e438dcdb90e4f5f35137/python_calamine-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81f243323bf712bb0b2baf0b938a2e6d6c9fa3b9902a44c0654474d04f999fac", size = 871527 }, - { url = "https://files.pythonhosted.org/packages/13/19/063afced19620b829697b90329c62ad73274cc38faaa91d9ee41047f5f8c/python_calamine-0.3.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b719dd2b10237b0cfb2062e3eaf199f220918a5623197e8449f37c8de845a7c", size = 875411 }, - { url = "https://files.pythonhosted.org/packages/d7/6a/c93c52414ec62cc51c4820aff434f03c4a1c69ced15cec3e4b93885e4012/python_calamine-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5158310b9140e8ee8665c9541a11030901e7275eb036988150c93f01c5133bf", size = 943525 }, - { url = "https://files.pythonhosted.org/packages/0a/0a/5bdecee03d235e8d111b1e8ee3ea0c0ed4ae43a402f75cebbe719930cf04/python_calamine-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2c1b248e8bf10194c449cb57e6ccb3f2fe3dc86975a6d746908cf2d37b048cc", size = 976332 }, - { url = "https://files.pythonhosted.org/packages/05/ad/43ff92366856ee34f958e9cf4f5b98e63b0dc219e06ccba4ad6f63463756/python_calamine-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a13ad8e5b6843a73933b8d1710bc4df39a9152cb57c11227ad51f47b5838a4", size = 885549 }, - { url = "https://files.pythonhosted.org/packages/ff/b9/76afb867e2bb4bfc296446b741cee01ae4ce6a094b43f4ed4eaed5189de4/python_calamine-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe950975a5758423c982ce1e2fdcb5c9c664d1a20b41ea21e619e5003bb4f96b", size = 926005 }, - { url = "https://files.pythonhosted.org/packages/23/cf/5252b237b0e70c263f86741aea02e8e57aedb2bce9898468be1d9d55b9da/python_calamine-0.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8707622ba816d6c26e36f1506ecda66a6a6cf43e55a43a8ef4c3bf8a805d3cfb", size = 1049380 }, - { url = "https://files.pythonhosted.org/packages/1a/4d/f151e8923e53457ca49ceeaa3a34cb23afee7d7b46e6546ab2a29adc9125/python_calamine-0.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e6eac46475c26e162a037f6711b663767f61f8fca3daffeb35aa3fc7ee6267cc", size = 1056720 }, - { url = "https://files.pythonhosted.org/packages/f5/cb/1b5db3e4a8bbaaaa7706b270570d4a65133618fa0ca7efafe5ce680f6cee/python_calamine-0.3.2-cp312-cp312-win32.whl", hash = "sha256:0dee82aedef3db27368a388d6741d69334c1d4d7a8087ddd33f1912166e17e37", size = 663502 }, - { url = "https://files.pythonhosted.org/packages/5a/53/920fa8e7b570647c08da0f1158d781db2e318918b06cb28fe0363c3398ac/python_calamine-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:ae09b779718809d31ca5d722464be2776b7d79278b1da56e159bbbe11880eecf", size = 692660 }, - { url = "https://files.pythonhosted.org/packages/a5/ea/5d0ecf5c345c4d78964a5f97e61848bc912965b276a54fb8ae698a9419a8/python_calamine-0.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:435546e401a5821fa70048b6c03a70db3b27d00037e2c4999c2126d8c40b51df", size = 666205 }, +sdist = { url = "https://files.pythonhosted.org/packages/cc/03/269f96535705b2f18c8977fa58e76763b4e4727a9b3ae277a9468c8ffe05/python_calamine-0.4.0.tar.gz", hash = "sha256:94afcbae3fec36d2d7475095a59d4dc6fae45829968c743cb799ebae269d7bbf", size = 127737, upload-time = "2025-07-04T06:05:28.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/a5/bcd82326d0ff1ab5889e7a5e13c868b483fc56398e143aae8e93149ba43b/python_calamine-0.4.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d1687f8c4d7852920c7b4e398072f183f88dd273baf5153391edc88b7454b8c0", size = 833019, upload-time = "2025-07-04T06:03:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1a/a681f1d2f28164552e91ef47bcde6708098aa64a5f5fe3952f22362d340a/python_calamine-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:258d04230bebbbafa370a15838049d912d6a0a2c4da128943d8160ca4b6db58e", size = 812268, upload-time = "2025-07-04T06:03:33.855Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/2fc911431733739d4e7a633cefa903fa49a6b7a61e8765bad29a4a7c47b1/python_calamine-0.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686e491634934f059553d55f77ac67ca4c235452d5b444f98fe79b3579f1ea5", size = 875733, upload-time = "2025-07-04T06:03:35.154Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/48bfae6802eb360028ca6c15e9edf42243aadd0006b6ac3e9edb41a57119/python_calamine-0.4.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4480af7babcc2f919c638a554b06b7b145d9ab3da47fd696d68c2fc6f67f9541", size = 878325, upload-time = "2025-07-04T06:03:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dc/f8c956e15bac9d5d1e05cd1b907ae780e40522d2fd103c8c6e2f21dff4ed/python_calamine-0.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e405b87a8cd1e90a994e570705898634f105442029f25bab7da658ee9cbaa771", size = 1015038, upload-time = "2025-07-04T06:03:37.971Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/e69ab97c7734fb850fba2f506b775912fd59f04e17488582c8fbf52dbc72/python_calamine-0.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a831345ee42615f0dfcb0ed60a3b1601d2f946d4166edae64fd9a6f9bbd57fc1", size = 924969, upload-time = "2025-07-04T06:03:39.253Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/b4c056b468908d87a3de94389166e0f4dba725a70bc39e03bc039ba96f6b/python_calamine-0.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9951b8e4cafb3e1623bb5dfc31a18d38ef43589275f9657e99dfcbe4c8c4b33e", size = 888020, upload-time = "2025-07-04T06:03:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/86/4f/b9092f7c970894054083656953184e44cb2dadff8852425e950d4ca419af/python_calamine-0.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a6619fe3b5c9633ed8b178684605f8076c9d8d85b29ade15f7a7713fcfdee2d0", size = 930337, upload-time = "2025-07-04T06:03:42.89Z" }, + { url = "https://files.pythonhosted.org/packages/64/da/137239027bf253aabe7063450950085ec9abd827d0cbc5170f585f38f464/python_calamine-0.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cc45b8e76ee331f6ea88ca23677be0b7a05b502cd4423ba2c2bc8dad53af1be", size = 1054568, upload-time = "2025-07-04T06:03:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/80/96/74c38bcf6b6825d5180c0e147b85be8c52dbfba11848b1e98ba358e32a64/python_calamine-0.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1b2cfb7ced1a7c80befa0cfddfe4aae65663eb4d63c4ae484b9b7a80ebe1b528", size = 1058317, upload-time = "2025-07-04T06:03:45.873Z" }, + { url = "https://files.pythonhosted.org/packages/33/95/9d7b8fe8b32d99a6c79534df3132cfe40e9df4a0f5204048bf5e66ddbd93/python_calamine-0.4.0-cp311-cp311-win32.whl", hash = "sha256:04f4e32ee16814fc1fafc49300be8eeb280d94878461634768b51497e1444bd6", size = 663934, upload-time = "2025-07-04T06:03:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e3/1c6cd9fd499083bea6ff1c30033ee8215b9f64e862babf5be170cacae190/python_calamine-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:a8543f69afac2213c0257bb56215b03dadd11763064a9d6b19786f27d1bef586", size = 692535, upload-time = "2025-07-04T06:03:48.699Z" }, + { url = "https://files.pythonhosted.org/packages/94/1c/3105d19fbab6b66874ce8831652caedd73b23b72e88ce18addf8ceca8c12/python_calamine-0.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:54622e35ec7c3b6f07d119da49aa821731c185e951918f152c2dbf3bec1e15d6", size = 671751, upload-time = "2025-07-04T06:03:49.979Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/f951513aaaa470b3a38a87d65eca45e0a02bc329b47864f5a17db563f746/python_calamine-0.4.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:74bca5d44a73acf3dcfa5370820797fcfd225c8c71abcddea987c5b4f5077e98", size = 826603, upload-time = "2025-07-04T06:03:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/76/3f/789955bbc77831c639890758f945eb2b25d6358065edf00da6751226cf31/python_calamine-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf80178f5d1b0ee2ccfffb8549c50855f6249e930664adc5807f4d0d6c2b269c", size = 805826, upload-time = "2025-07-04T06:03:52.482Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/f87d17d996f647030a40bfd124fe45fe893c002bee35ae6aca9910a923ae/python_calamine-0.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65cfef345386ae86f7720f1be93495a40fd7e7feabb8caa1df5025d7fbc58a1f", size = 874989, upload-time = "2025-07-04T06:03:53.794Z" }, + { url = "https://files.pythonhosted.org/packages/47/d2/3269367303f6c0488cf1bfebded3f9fe968d118a988222e04c9b2636bf2e/python_calamine-0.4.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f23e6214dbf9b29065a5dcfd6a6c674dd0e251407298c9138611c907d53423ff", size = 877504, upload-time = "2025-07-04T06:03:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/f9/6d/c7ac35f5c7125e8bd07eb36773f300fda20dd2da635eae78a8cebb0b6ab7/python_calamine-0.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d792d304ee232ab01598e1d3ab22e074a32c2511476b5fb4f16f4222d9c2a265", size = 1014171, upload-time = "2025-07-04T06:03:56.777Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/5ea8792a2e9ab5e2a05872db3a4d3ed3538ad5af1861282c789e2f13a8cf/python_calamine-0.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf813425918fd68f3e991ef7c4b5015be0a1a95fc4a8ab7e73c016ef1b881bb4", size = 926737, upload-time = "2025-07-04T06:03:58.024Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/989e56e6f073fc0981a74ba7a393881eb351bb143e5486aa629b5e5d6a8b/python_calamine-0.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbe2a0ccb4d003635888eea83a995ff56b0748c8c76fc71923544f5a4a7d4cd7", size = 887032, upload-time = "2025-07-04T06:03:59.298Z" }, + { url = "https://files.pythonhosted.org/packages/5d/92/2c9bd64277c6fe4be695d7d5a803b38d953ec8565037486be7506642c27c/python_calamine-0.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7b3bb5f0d910b9b03c240987560f843256626fd443279759df4e91b717826d2", size = 929700, upload-time = "2025-07-04T06:04:01.388Z" }, + { url = "https://files.pythonhosted.org/packages/64/fa/fc758ca37701d354a6bc7d63118699f1c73788a1f2e1b44d720824992764/python_calamine-0.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bd2c0fc2b5eabd08ceac8a2935bffa88dbc6116db971aa8c3f244bad3fd0f644", size = 1053971, upload-time = "2025-07-04T06:04:02.704Z" }, + { url = "https://files.pythonhosted.org/packages/65/52/40d7e08ae0ddba331cdc9f7fb3e92972f8f38d7afbd00228158ff6d1fceb/python_calamine-0.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:85b547cb1c5b692a0c2406678d666dbc1cec65a714046104683fe4f504a1721d", size = 1057057, upload-time = "2025-07-04T06:04:04.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/de/e8a071c0adfda73285d891898a24f6e99338328c404f497ff5b0e6bc3d45/python_calamine-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4c2a1e3a0db4d6de4587999a21cc35845648c84fba81c03dd6f3072c690888e4", size = 665540, upload-time = "2025-07-04T06:04:05.679Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f2/7fdfada13f80db12356853cf08697ff4e38800a1809c2bdd26ee60962e7a/python_calamine-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b193c89ffcc146019475cd121c552b23348411e19c04dedf5c766a20db64399a", size = 695366, upload-time = "2025-07-04T06:04:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/20/66/d37412ad854480ce32f50d9f74f2a2f88b1b8a6fbc32f70aabf3211ae89e/python_calamine-0.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:43a0f15e0b60c75a71b21a012b911d5d6f5fa052afad2a8edbc728af43af0fcf", size = 670740, upload-time = "2025-07-04T06:04:08.656Z" }, ] [[package]] @@ -4589,9 +4649,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] @@ -4602,36 +4662,45 @@ dependencies = [ { name = "lxml" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/e4/386c514c53684772885009c12b67a7edd526c15157778ac1b138bc75063e/python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd", size = 5656581 } +sdist = { url = "https://files.pythonhosted.org/packages/35/e4/386c514c53684772885009c12b67a7edd526c15157778ac1b138bc75063e/python_docx-1.1.2.tar.gz", hash = "sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd", size = 5656581, upload-time = "2024-05-01T19:41:57.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/3d/330d9efbdb816d3f60bf2ad92f05e1708e4a1b9abe80461ac3444c83f749/python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe", size = 244315 }, + { url = "https://files.pythonhosted.org/packages/3e/3d/330d9efbdb816d3f60bf2ad92f05e1708e4a1b9abe80461ac3444c83f749/python_docx-1.1.2-py3-none-any.whl", hash = "sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe", size = 244315, upload-time = "2024-05-01T19:41:47.006Z" }, ] [[package]] name = "python-dotenv" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, +] + +[[package]] +name = "python-http-client" +version = "3.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/fa/284e52a8c6dcbe25671f02d217bf2f85660db940088faf18ae7a05e97313/python_http_client-3.3.7.tar.gz", hash = "sha256:bf841ee45262747e00dec7ee9971dfb8c7d83083f5713596488d67739170cea0", size = 9377, upload-time = "2022-03-09T20:23:56.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, + { url = "https://files.pythonhosted.org/packages/29/31/9b360138f4e4035ee9dac4fe1132b6437bd05751aaf1db2a2d83dc45db5f/python_http_client-3.3.7-py3-none-any.whl", hash = "sha256:ad371d2bbedc6ea15c26179c6222a78bc9308d272435ddf1d5c84f068f249a36", size = 8352, upload-time = "2022-03-09T20:23:54.862Z" }, ] [[package]] name = "python-iso639" version = "2025.2.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/19/45aa1917c7b1f4eb71104795b9b0cbf97169b99ec46cd303445883536549/python_iso639-2025.2.18.tar.gz", hash = "sha256:34e31e8e76eb3fc839629e257b12bcfd957c6edcbd486bbf66ba5185d1f566e8", size = 173552 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/19/45aa1917c7b1f4eb71104795b9b0cbf97169b99ec46cd303445883536549/python_iso639-2025.2.18.tar.gz", hash = "sha256:34e31e8e76eb3fc839629e257b12bcfd957c6edcbd486bbf66ba5185d1f566e8", size = 173552, upload-time = "2025-02-18T13:48:08.607Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/a3/3ceaf89a17a1e1d5e7bbdfe5514aa3055d91285b37a5c8fed662969e3d56/python_iso639-2025.2.18-py3-none-any.whl", hash = "sha256:b2d471c37483a26f19248458b20e7bd96492e15368b01053b540126bcc23152f", size = 167631 }, + { url = "https://files.pythonhosted.org/packages/54/a3/3ceaf89a17a1e1d5e7bbdfe5514aa3055d91285b37a5c8fed662969e3d56/python_iso639-2025.2.18-py3-none-any.whl", hash = "sha256:b2d471c37483a26f19248458b20e7bd96492e15368b01053b540126bcc23152f", size = 167631, upload-time = "2025-02-18T13:48:06.602Z" }, ] [[package]] name = "python-magic" version = "0.4.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 } +sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 }, + { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, ] [[package]] @@ -4643,9 +4712,9 @@ dependencies = [ { name = "olefile" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/4e/869f34faedbc968796d2c7e9837dede079c9cb9750917356b1f1eda926e9/python_oxmsg-0.0.2.tar.gz", hash = "sha256:a6aff4deb1b5975d44d49dab1d9384089ffeec819e19c6940bc7ffbc84775fad", size = 34713 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/4e/869f34faedbc968796d2c7e9837dede079c9cb9750917356b1f1eda926e9/python_oxmsg-0.0.2.tar.gz", hash = "sha256:a6aff4deb1b5975d44d49dab1d9384089ffeec819e19c6940bc7ffbc84775fad", size = 34713, upload-time = "2025-02-03T17:13:47.415Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455 }, + { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455, upload-time = "2025-02-03T17:13:46.061Z" }, ] [[package]] @@ -4658,18 +4727,18 @@ dependencies = [ { name = "typing-extensions" }, { name = "xlsxwriter" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297 } +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788 }, + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] @@ -4677,47 +4746,47 @@ name = "pywin32" version = "310" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284 }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748 }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941 }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239 }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839 }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470 }, + { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, ] [[package]] name = "pyxlsb" version = "1.0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/13/eebaeb7a40b062d1c6f7f91d09e73d30a69e33e4baa7cbe4b7658548b1cd/pyxlsb-1.0.10.tar.gz", hash = "sha256:8062d1ea8626d3f1980e8b1cfe91a4483747449242ecb61013bc2df85435f685", size = 22424 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/13/eebaeb7a40b062d1c6f7f91d09e73d30a69e33e4baa7cbe4b7658548b1cd/pyxlsb-1.0.10.tar.gz", hash = "sha256:8062d1ea8626d3f1980e8b1cfe91a4483747449242ecb61013bc2df85435f685", size = 22424, upload-time = "2022-10-14T19:17:47.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/92/345823838ae367c59b63e03aef9c331f485370f9df6d049256a61a28f06d/pyxlsb-1.0.10-py2.py3-none-any.whl", hash = "sha256:87c122a9a622e35ca5e741d2e541201d28af00fb46bec492cfa9586890b120b4", size = 23849 }, + { url = "https://files.pythonhosted.org/packages/7e/92/345823838ae367c59b63e03aef9c331f485370f9df6d049256a61a28f06d/pyxlsb-1.0.10-py2.py3-none-any.whl", hash = "sha256:87c122a9a622e35ca5e741d2e541201d28af00fb46bec492cfa9586890b120b4", size = 23849, upload-time = "2022-10-14T19:17:46.079Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, ] [[package]] @@ -4733,53 +4802,53 @@ dependencies = [ { name = "pydantic" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/cf/db06a74694bf8f126ed4a869c70ef576f01ee691ef20799fba3d561d3565/qdrant_client-1.9.0.tar.gz", hash = "sha256:7b1792f616651a6f0a76312f945c13d088e9451726795b82ce0350f7df3b7981", size = 199999 } +sdist = { url = "https://files.pythonhosted.org/packages/86/cf/db06a74694bf8f126ed4a869c70ef576f01ee691ef20799fba3d561d3565/qdrant_client-1.9.0.tar.gz", hash = "sha256:7b1792f616651a6f0a76312f945c13d088e9451726795b82ce0350f7df3b7981", size = 199999, upload-time = "2024-04-22T13:35:49.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/fa/5abd82cde353f1009c068cca820195efd94e403d261b787e78ea7a9c8318/qdrant_client-1.9.0-py3-none-any.whl", hash = "sha256:ee02893eab1f642481b1ac1e38eb68ec30bab0f673bef7cc05c19fa5d2cbf43e", size = 229258 }, + { url = "https://files.pythonhosted.org/packages/3a/fa/5abd82cde353f1009c068cca820195efd94e403d261b787e78ea7a9c8318/qdrant_client-1.9.0-py3-none-any.whl", hash = "sha256:ee02893eab1f642481b1ac1e38eb68ec30bab0f673bef7cc05c19fa5d2cbf43e", size = 229258, upload-time = "2024-04-22T13:35:46.81Z" }, ] [[package]] name = "rapidfuzz" version = "3.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453 }, - { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881 }, - { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990 }, - { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309 }, - { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881 }, - { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494 }, - { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160 }, - { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549 }, - { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142 }, - { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234 }, - { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420 }, - { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860 }, - { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161 }, - { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962 }, - { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631 }, - { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501 }, - { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379 }, - { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986 }, - { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809 }, - { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394 }, - { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544 }, - { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796 }, - { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016 }, - { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725 }, - { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052 }, - { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219 }, - { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924 }, - { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915 }, - { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985 }, - { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116 }, - { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935 }, - { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714 }, - { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329 }, - { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057 }, - { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401 }, - { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782 }, +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226, upload-time = "2025-04-03T20:38:51.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453, upload-time = "2025-04-03T20:35:40.804Z" }, + { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881, upload-time = "2025-04-03T20:35:42.734Z" }, + { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990, upload-time = "2025-04-03T20:35:45.158Z" }, + { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309, upload-time = "2025-04-03T20:35:46.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881, upload-time = "2025-04-03T20:35:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494, upload-time = "2025-04-03T20:35:51.646Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160, upload-time = "2025-04-03T20:35:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549, upload-time = "2025-04-03T20:35:55.391Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142, upload-time = "2025-04-03T20:35:57.71Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234, upload-time = "2025-04-03T20:35:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420, upload-time = "2025-04-03T20:36:01.91Z" }, + { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860, upload-time = "2025-04-03T20:36:04.352Z" }, + { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161, upload-time = "2025-04-03T20:36:06.802Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962, upload-time = "2025-04-03T20:36:09.133Z" }, + { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631, upload-time = "2025-04-03T20:36:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501, upload-time = "2025-04-03T20:36:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379, upload-time = "2025-04-03T20:36:16.439Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986, upload-time = "2025-04-03T20:36:18.447Z" }, + { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809, upload-time = "2025-04-03T20:36:20.324Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394, upload-time = "2025-04-03T20:36:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544, upload-time = "2025-04-03T20:36:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796, upload-time = "2025-04-03T20:36:26.279Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016, upload-time = "2025-04-03T20:36:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725, upload-time = "2025-04-03T20:36:30.629Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052, upload-time = "2025-04-03T20:36:32.836Z" }, + { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219, upload-time = "2025-04-03T20:36:35.062Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924, upload-time = "2025-04-03T20:36:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915, upload-time = "2025-04-03T20:36:39.451Z" }, + { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985, upload-time = "2025-04-03T20:36:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116, upload-time = "2025-04-03T20:36:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935, upload-time = "2025-04-03T20:38:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714, upload-time = "2025-04-03T20:38:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329, upload-time = "2025-04-03T20:38:23.01Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057, upload-time = "2025-04-03T20:38:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401, upload-time = "2025-04-03T20:38:28.196Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782, upload-time = "2025-04-03T20:38:30.778Z" }, ] [[package]] @@ -4792,24 +4861,22 @@ dependencies = [ { name = "lxml" }, { name = "regex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/e4/260a202516886c2e0cc6e6ae96d1f491792d829098886d9529a2439fbe8e/readabilipy-0.3.0.tar.gz", hash = "sha256:e13313771216953935ac031db4234bdb9725413534bfb3c19dbd6caab0887ae0", size = 35491 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/e4/260a202516886c2e0cc6e6ae96d1f491792d829098886d9529a2439fbe8e/readabilipy-0.3.0.tar.gz", hash = "sha256:e13313771216953935ac031db4234bdb9725413534bfb3c19dbd6caab0887ae0", size = 35491, upload-time = "2024-12-02T23:03:02.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/46/8a640c6de1a6c6af971f858b2fb178ca5e1db91f223d8ba5f40efe1491e5/readabilipy-0.3.0-py3-none-any.whl", hash = "sha256:d106da0fad11d5fdfcde21f5c5385556bfa8ff0258483037d39ea6b1d6db3943", size = 22158 }, + { url = "https://files.pythonhosted.org/packages/dd/46/8a640c6de1a6c6af971f858b2fb178ca5e1db91f223d8ba5f40efe1491e5/readabilipy-0.3.0-py3-none-any.whl", hash = "sha256:d106da0fad11d5fdfcde21f5c5385556bfa8ff0258483037d39ea6b1d6db3943", size = 22158, upload-time = "2024-12-02T23:03:00.438Z" }, ] [[package]] name = "realtime" -version = "2.5.0" +version = "2.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "python-dateutil" }, { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/9e/d6e478ccc23869a450a5d0dd9ab0c8a4e37fee7aec43c925d89bb09fcaf5/realtime-2.5.0.tar.gz", hash = "sha256:03d744dedc823de019a7f9917c1a6509fb6e98d357adf7fd7f4015618dac7ecd", size = 18865 } +sdist = { url = "https://files.pythonhosted.org/packages/48/94/3cf962b814303a1688eece56a94b25a7bd423d60705f1124cba0896c9c07/realtime-2.5.3.tar.gz", hash = "sha256:0587594f3bc1c84bf007ff625075b86db6528843e03250dc84f4f2808be3d99a", size = 18527, upload-time = "2025-06-26T22:39:01.59Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/09/a9ede9afa4a536ac84ad365bfa26116ab17463c8353f471b2396dc0e44d0/realtime-2.5.0-py3-none-any.whl", hash = "sha256:a54274a6cdc03c3eda61fbfec1d277e4a28e3aa9526d24b5c187385bb8a7e85a", size = 22086 }, + { url = "https://files.pythonhosted.org/packages/fe/2a/f69c156a58d44b7b9ca22dab181b91e4d93d074f99923c75907bf3953d40/realtime-2.5.3-py3-none-any.whl", hash = "sha256:eb0994636946eff04c4c7f044f980c8c633c7eb632994f549f61053a474ac970", size = 21784, upload-time = "2025-06-26T22:38:59.98Z" }, ] [[package]] @@ -4819,9 +4886,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/8b/14ef373ffe71c0d2fde93c204eab78472ea13c021d9aee63b0e11bd65896/redis-6.1.1.tar.gz", hash = "sha256:88c689325b5b41cedcbdbdfd4d937ea86cf6dab2222a83e86d8a466e4b3d2600", size = 4629515 } +sdist = { url = "https://files.pythonhosted.org/packages/07/8b/14ef373ffe71c0d2fde93c204eab78472ea13c021d9aee63b0e11bd65896/redis-6.1.1.tar.gz", hash = "sha256:88c689325b5b41cedcbdbdfd4d937ea86cf6dab2222a83e86d8a466e4b3d2600", size = 4629515, upload-time = "2025-06-02T11:44:04.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/cd/29503c609186104c363ef1f38d6e752e7d91ef387fc90aa165e96d69f446/redis-6.1.1-py3-none-any.whl", hash = "sha256:ed44d53d065bbe04ac6d76864e331cfe5c5353f86f6deccc095f8794fd15bb2e", size = 273930 }, + { url = "https://files.pythonhosted.org/packages/c2/cd/29503c609186104c363ef1f38d6e752e7d91ef387fc90aa165e96d69f446/redis-6.1.1-py3-none-any.whl", hash = "sha256:ed44d53d065bbe04ac6d76864e331cfe5c5353f86f6deccc095f8794fd15bb2e", size = 273930, upload-time = "2025-06-02T11:44:02.705Z" }, ] [package.optional-dependencies] @@ -4838,52 +4905,52 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] name = "regex" version = "2024.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4891,9 +4958,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] @@ -4904,9 +4971,9 @@ dependencies = [ { name = "oauthlib" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] [[package]] @@ -4916,9 +4983,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] @@ -4929,9 +4996,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/2a/535a794e5b64f6ef4abc1342ef1a43465af2111c5185e98b4cca2a6b6b7a/resend-2.9.0.tar.gz", hash = "sha256:e8d4c909a7fe7701119789f848a6befb0a4a668e2182d7bbfe764742f1952bd3", size = 13600 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/2a/535a794e5b64f6ef4abc1342ef1a43465af2111c5185e98b4cca2a6b6b7a/resend-2.9.0.tar.gz", hash = "sha256:e8d4c909a7fe7701119789f848a6befb0a4a668e2182d7bbfe764742f1952bd3", size = 13600, upload-time = "2025-05-06T00:35:20.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/81/ba1feb9959bafbcde6466b78d4628405d69cd14613f6eba12b928a77b86a/resend-2.9.0-py2.py3-none-any.whl", hash = "sha256:6607f75e3a9257a219c0640f935b8d1211338190d553eb043c25732affb92949", size = 20173 }, + { url = "https://files.pythonhosted.org/packages/96/81/ba1feb9959bafbcde6466b78d4628405d69cd14613f6eba12b928a77b86a/resend-2.9.0-py2.py3-none-any.whl", hash = "sha256:6607f75e3a9257a219c0640f935b8d1211338190d553eb043c25732affb92949", size = 20173, upload-time = "2025-05-06T00:35:18.963Z" }, ] [[package]] @@ -4942,9 +5009,9 @@ dependencies = [ { name = "decorator" }, { name = "py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448, upload-time = "2016-05-11T13:58:51.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986 }, + { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986, upload-time = "2016-05-11T13:58:39.925Z" }, ] [[package]] @@ -4955,56 +5022,56 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] name = "rpds-py" -version = "0.25.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/e1/df13fe3ddbbea43567e07437f097863b20c99318ae1f58a0fe389f763738/rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d", size = 373341 }, - { url = "https://files.pythonhosted.org/packages/7a/58/deef4d30fcbcbfef3b6d82d17c64490d5c94585a2310544ce8e2d3024f83/rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255", size = 359111 }, - { url = "https://files.pythonhosted.org/packages/bb/7e/39f1f4431b03e96ebaf159e29a0f82a77259d8f38b2dd474721eb3a8ac9b/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2", size = 386112 }, - { url = "https://files.pythonhosted.org/packages/db/e7/847068a48d63aec2ae695a1646089620b3b03f8ccf9f02c122ebaf778f3c/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0", size = 400362 }, - { url = "https://files.pythonhosted.org/packages/3b/3d/9441d5db4343d0cee759a7ab4d67420a476cebb032081763de934719727b/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f", size = 522214 }, - { url = "https://files.pythonhosted.org/packages/a2/ec/2cc5b30d95f9f1a432c79c7a2f65d85e52812a8f6cbf8768724571710786/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7", size = 411491 }, - { url = "https://files.pythonhosted.org/packages/dc/6c/44695c1f035077a017dd472b6a3253553780837af2fac9b6ac25f6a5cb4d/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd", size = 386978 }, - { url = "https://files.pythonhosted.org/packages/b1/74/b4357090bb1096db5392157b4e7ed8bb2417dc7799200fcbaee633a032c9/rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65", size = 420662 }, - { url = "https://files.pythonhosted.org/packages/26/dd/8cadbebf47b96e59dfe8b35868e5c38a42272699324e95ed522da09d3a40/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f", size = 563385 }, - { url = "https://files.pythonhosted.org/packages/c3/ea/92960bb7f0e7a57a5ab233662f12152085c7dc0d5468534c65991a3d48c9/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d", size = 592047 }, - { url = "https://files.pythonhosted.org/packages/61/ad/71aabc93df0d05dabcb4b0c749277881f8e74548582d96aa1bf24379493a/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042", size = 557863 }, - { url = "https://files.pythonhosted.org/packages/93/0f/89df0067c41f122b90b76f3660028a466eb287cbe38efec3ea70e637ca78/rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc", size = 219627 }, - { url = "https://files.pythonhosted.org/packages/7c/8d/93b1a4c1baa903d0229374d9e7aa3466d751f1d65e268c52e6039c6e338e/rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4", size = 231603 }, - { url = "https://files.pythonhosted.org/packages/cb/11/392605e5247bead2f23e6888e77229fbd714ac241ebbebb39a1e822c8815/rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4", size = 223967 }, - { url = "https://files.pythonhosted.org/packages/7f/81/28ab0408391b1dc57393653b6a0cf2014cc282cc2909e4615e63e58262be/rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", size = 364647 }, - { url = "https://files.pythonhosted.org/packages/2c/9a/7797f04cad0d5e56310e1238434f71fc6939d0bc517192a18bb99a72a95f/rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", size = 350454 }, - { url = "https://files.pythonhosted.org/packages/69/3c/93d2ef941b04898011e5d6eaa56a1acf46a3b4c9f4b3ad1bbcbafa0bee1f/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", size = 389665 }, - { url = "https://files.pythonhosted.org/packages/c1/57/ad0e31e928751dde8903a11102559628d24173428a0f85e25e187defb2c1/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", size = 403873 }, - { url = "https://files.pythonhosted.org/packages/16/ad/c0c652fa9bba778b4f54980a02962748479dc09632e1fd34e5282cf2556c/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", size = 525866 }, - { url = "https://files.pythonhosted.org/packages/2a/39/3e1839bc527e6fcf48d5fec4770070f872cdee6c6fbc9b259932f4e88a38/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", size = 416886 }, - { url = "https://files.pythonhosted.org/packages/7a/95/dd6b91cd4560da41df9d7030a038298a67d24f8ca38e150562644c829c48/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", size = 390666 }, - { url = "https://files.pythonhosted.org/packages/64/48/1be88a820e7494ce0a15c2d390ccb7c52212370badabf128e6a7bb4cb802/rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", size = 425109 }, - { url = "https://files.pythonhosted.org/packages/cf/07/3e2a17927ef6d7720b9949ec1b37d1e963b829ad0387f7af18d923d5cfa5/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", size = 567244 }, - { url = "https://files.pythonhosted.org/packages/d2/e5/76cf010998deccc4f95305d827847e2eae9c568099c06b405cf96384762b/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", size = 596023 }, - { url = "https://files.pythonhosted.org/packages/52/9a/df55efd84403736ba37a5a6377b70aad0fd1cb469a9109ee8a1e21299a1c/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", size = 561634 }, - { url = "https://files.pythonhosted.org/packages/ab/aa/dc3620dd8db84454aaf9374bd318f1aa02578bba5e567f5bf6b79492aca4/rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", size = 222713 }, - { url = "https://files.pythonhosted.org/packages/a3/7f/7cef485269a50ed5b4e9bae145f512d2a111ca638ae70cc101f661b4defd/rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", size = 235280 }, - { url = "https://files.pythonhosted.org/packages/99/f2/c2d64f6564f32af913bf5f3f7ae41c7c263c5ae4c4e8f1a17af8af66cd46/rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", size = 225399 }, - { url = "https://files.pythonhosted.org/packages/49/74/48f3df0715a585cbf5d34919c9c757a4c92c1a9eba059f2d334e72471f70/rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954", size = 374208 }, - { url = "https://files.pythonhosted.org/packages/55/b0/9b01bb11ce01ec03d05e627249cc2c06039d6aa24ea5a22a39c312167c10/rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba", size = 359262 }, - { url = "https://files.pythonhosted.org/packages/a9/eb/5395621618f723ebd5116c53282052943a726dba111b49cd2071f785b665/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b", size = 387366 }, - { url = "https://files.pythonhosted.org/packages/68/73/3d51442bdb246db619d75039a50ea1cf8b5b4ee250c3e5cd5c3af5981cd4/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038", size = 400759 }, - { url = "https://files.pythonhosted.org/packages/b7/4c/3a32d5955d7e6cb117314597bc0f2224efc798428318b13073efe306512a/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9", size = 523128 }, - { url = "https://files.pythonhosted.org/packages/be/95/1ffccd3b0bb901ae60b1dd4b1be2ab98bb4eb834cd9b15199888f5702f7b/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1", size = 411597 }, - { url = "https://files.pythonhosted.org/packages/ef/6d/6e6cd310180689db8b0d2de7f7d1eabf3fb013f239e156ae0d5a1a85c27f/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762", size = 388053 }, - { url = "https://files.pythonhosted.org/packages/4a/87/ec4186b1fe6365ced6fa470960e68fc7804bafbe7c0cf5a36237aa240efa/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e", size = 421821 }, - { url = "https://files.pythonhosted.org/packages/7a/60/84f821f6bf4e0e710acc5039d91f8f594fae0d93fc368704920d8971680d/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692", size = 564534 }, - { url = "https://files.pythonhosted.org/packages/41/3a/bc654eb15d3b38f9330fe0f545016ba154d89cdabc6177b0295910cd0ebe/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf", size = 592674 }, - { url = "https://files.pythonhosted.org/packages/2e/ba/31239736f29e4dfc7a58a45955c5db852864c306131fd6320aea214d5437/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe", size = 558781 }, +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" }, ] [[package]] @@ -5014,34 +5081,34 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] name = "ruff" -version = "0.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597 }, - { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154 }, - { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048 }, - { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062 }, - { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152 }, - { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067 }, - { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807 }, - { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261 }, - { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601 }, - { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186 }, - { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032 }, - { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370 }, - { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529 }, - { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642 }, - { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511 }, - { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573 }, - { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770 }, +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, + { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, ] [[package]] @@ -5051,31 +5118,57 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287, upload-time = "2024-11-20T21:06:05.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175 }, + { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175, upload-time = "2024-11-20T21:06:03.961Z" }, ] [[package]] name = "safetensors" version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210 } +sdist = { url = "https://files.pythonhosted.org/packages/71/7e/2d5d6ee7b40c0682315367ec7475693d110f512922d582fef1bd4a63adc3/safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965", size = 67210, upload-time = "2025-02-26T09:15:13.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917, upload-time = "2025-02-26T09:15:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419, upload-time = "2025-02-26T09:15:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493, upload-time = "2025-02-26T09:14:51.812Z" }, + { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400, upload-time = "2025-02-26T09:14:53.549Z" }, + { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891, upload-time = "2025-02-26T09:14:55.717Z" }, + { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694, upload-time = "2025-02-26T09:14:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642, upload-time = "2025-02-26T09:15:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241, upload-time = "2025-02-26T09:14:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001, upload-time = "2025-02-26T09:15:05.79Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013, upload-time = "2025-02-26T09:15:07.892Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687, upload-time = "2025-02-26T09:15:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147, upload-time = "2025-02-26T09:15:11.185Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677, upload-time = "2025-02-26T09:15:16.554Z" }, + { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878, upload-time = "2025-02-26T09:15:14.99Z" }, +] + +[[package]] +name = "scipy-stubs" +version = "1.16.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "optype" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/19/a8461383f7328300e83c34f58bf38ccc05f57c2289c0e54e2bea757de83c/scipy_stubs-1.16.0.2.tar.gz", hash = "sha256:f83aacaf2e899d044de6483e6112bf7a1942d683304077bc9e78cf6f21353acd", size = 306747, upload-time = "2025-07-01T23:19:04.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/30/b73418e6d3d8209fef684841d9a0e5b439d3528fa341a23b632fe47918dd/scipy_stubs-1.16.0.2-py3-none-any.whl", hash = "sha256:dc364d24a3accd1663e7576480bdb720533f94de8a05590354ff6d4a83d765c7", size = 491346, upload-time = "2025-07-01T23:19:03.222Z" }, +] + +[[package]] +name = "sendgrid" +version = "6.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "python-http-client" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/31/62e00433878dccf33edf07f8efa417b9030a2464eb3b04bbd797a11b4447/sendgrid-6.12.4.tar.gz", hash = "sha256:9e88b849daf0fa4bdf256c3b5da9f5a3272402c0c2fd6b1928c9de440db0a03d", size = 50271, upload-time = "2025-06-12T10:29:37.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/ae/88f6c49dbd0cc4da0e08610019a3c78a7d390879a919411a410a1876d03a/safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073", size = 436917 }, - { url = "https://files.pythonhosted.org/packages/b8/3b/11f1b4a2f5d2ab7da34ecc062b0bc301f2be024d110a6466726bec8c055c/safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7", size = 418419 }, - { url = "https://files.pythonhosted.org/packages/5d/9a/add3e6fef267658075c5a41573c26d42d80c935cdc992384dfae435feaef/safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467", size = 459493 }, - { url = "https://files.pythonhosted.org/packages/df/5c/bf2cae92222513cc23b3ff85c4a1bb2811a2c3583ac0f8e8d502751de934/safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e", size = 472400 }, - { url = "https://files.pythonhosted.org/packages/58/11/7456afb740bd45782d0f4c8e8e1bb9e572f1bf82899fb6ace58af47b4282/safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d", size = 522891 }, - { url = "https://files.pythonhosted.org/packages/57/3d/fe73a9d2ace487e7285f6e157afee2383bd1ddb911b7cb44a55cf812eae3/safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9", size = 537694 }, - { url = "https://files.pythonhosted.org/packages/a6/f8/dae3421624fcc87a89d42e1898a798bc7ff72c61f38973a65d60df8f124c/safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a", size = 471642 }, - { url = "https://files.pythonhosted.org/packages/ce/20/1fbe16f9b815f6c5a672f5b760951e20e17e43f67f231428f871909a37f6/safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d", size = 502241 }, - { url = "https://files.pythonhosted.org/packages/5f/18/8e108846b506487aa4629fe4116b27db65c3dde922de2c8e0cc1133f3f29/safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b", size = 638001 }, - { url = "https://files.pythonhosted.org/packages/82/5a/c116111d8291af6c8c8a8b40628fe833b9db97d8141c2a82359d14d9e078/safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff", size = 734013 }, - { url = "https://files.pythonhosted.org/packages/7d/ff/41fcc4d3b7de837963622e8610d998710705bbde9a8a17221d85e5d0baad/safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135", size = 670687 }, - { url = "https://files.pythonhosted.org/packages/40/ad/2b113098e69c985a3d8fbda4b902778eae4a35b7d5188859b4a63d30c161/safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04", size = 643147 }, - { url = "https://files.pythonhosted.org/packages/0a/0c/95aeb51d4246bd9a3242d3d8349c1112b4ee7611a4b40f0c5c93b05f001d/safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace", size = 296677 }, - { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878 }, + { url = "https://files.pythonhosted.org/packages/c2/9c/45d068fd831a65e6ed1e2ab3233de58784842afdc62fdcdd0a01bbb6b39d/sendgrid-6.12.4-py3-none-any.whl", hash = "sha256:9a211b96241e63bd5b9ed9afcc8608f4bcac426e4a319b3920ab877c8426e92c", size = 102122, upload-time = "2025-06-12T10:29:35.457Z" }, ] [[package]] @@ -5086,9 +5179,9 @@ dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/bb/6a41b2e0e9121bed4d2ec68d50568ab95c49f4744156a9bbb789c866c66d/sentry_sdk-2.28.0.tar.gz", hash = "sha256:14d2b73bc93afaf2a9412490329099e6217761cbab13b6ee8bc0e82927e1504e", size = 325052 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/bb/6a41b2e0e9121bed4d2ec68d50568ab95c49f4744156a9bbb789c866c66d/sentry_sdk-2.28.0.tar.gz", hash = "sha256:14d2b73bc93afaf2a9412490329099e6217761cbab13b6ee8bc0e82927e1504e", size = 325052, upload-time = "2025-05-12T07:53:12.785Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4e/b1575833094c088dfdef63fbca794518860fcbc8002aadf51ebe8b6a387f/sentry_sdk-2.28.0-py2.py3-none-any.whl", hash = "sha256:51496e6cb3cb625b99c8e08907c67a9112360259b0ef08470e532c3ab184a232", size = 341693 }, + { url = "https://files.pythonhosted.org/packages/9b/4e/b1575833094c088dfdef63fbca794518860fcbc8002aadf51ebe8b6a387f/sentry_sdk-2.28.0-py2.py3-none-any.whl", hash = "sha256:51496e6cb3cb625b99c8e08907c67a9112360259b0ef08470e532c3ab184a232", size = 341693, upload-time = "2025-05-12T07:53:10.882Z" }, ] [package.optional-dependencies] @@ -5098,45 +5191,13 @@ flask = [ { name = "markupsafe" }, ] -[[package]] -name = "setproctitle" -version = "1.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/af/56efe21c53ac81ac87e000b15e60b3d8104224b4313b6eacac3597bd183d/setproctitle-1.3.6.tar.gz", hash = "sha256:c9f32b96c700bb384f33f7cf07954bb609d35dd82752cef57fb2ee0968409169", size = 26889 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/3b/8288d0cd969a63500dd62fc2c99ce6980f9909ccef0770ab1f86c361e0bf/setproctitle-1.3.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a1d856b0f4e4a33e31cdab5f50d0a14998f3a2d726a3fd5cb7c4d45a57b28d1b", size = 17412 }, - { url = "https://files.pythonhosted.org/packages/39/37/43a5a3e25ca1048dbbf4db0d88d346226f5f1acd131bb8e660f4bfe2799f/setproctitle-1.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50706b9c0eda55f7de18695bfeead5f28b58aa42fd5219b3b1692d554ecbc9ec", size = 11963 }, - { url = "https://files.pythonhosted.org/packages/5b/47/f103c40e133154783c91a10ab08ac9fc410ed835aa85bcf7107cb882f505/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af188f3305f0a65c3217c30c6d4c06891e79144076a91e8b454f14256acc7279", size = 31718 }, - { url = "https://files.pythonhosted.org/packages/1f/13/7325dd1c008dd6c0ebd370ddb7505977054a87e406f142318e395031a792/setproctitle-1.3.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce0ed8b3f64c71c140f0ec244e5fdf8ecf78ddf8d2e591d4a8b6aa1c1214235", size = 33027 }, - { url = "https://files.pythonhosted.org/packages/0c/0a/6075bfea05a71379d77af98a9ac61163e8b6e5ef1ae58cd2b05871b2079c/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70100e2087fe05359f249a0b5f393127b3a1819bf34dec3a3e0d4941138650c9", size = 30223 }, - { url = "https://files.pythonhosted.org/packages/cc/41/fbf57ec52f4f0776193bd94334a841f0bc9d17e745f89c7790f336420c65/setproctitle-1.3.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1065ed36bd03a3fd4186d6c6de5f19846650b015789f72e2dea2d77be99bdca1", size = 31204 }, - { url = "https://files.pythonhosted.org/packages/97/b5/f799fb7a00de29fb0ac1dfd015528dea425b9e31a8f1068a0b3df52d317f/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4adf6a0013fe4e0844e3ba7583ec203ca518b9394c6cc0d3354df2bf31d1c034", size = 31181 }, - { url = "https://files.pythonhosted.org/packages/b5/b7/81f101b612014ec61723436022c31146178813d6ca6b947f7b9c84e9daf4/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb7452849f6615871eabed6560ffedfe56bc8af31a823b6be4ce1e6ff0ab72c5", size = 30101 }, - { url = "https://files.pythonhosted.org/packages/67/23/681232eed7640eab96719daa8647cc99b639e3daff5c287bd270ef179a73/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a094b7ce455ca341b59a0f6ce6be2e11411ba6e2860b9aa3dbb37468f23338f4", size = 32438 }, - { url = "https://files.pythonhosted.org/packages/19/f8/4d075a7bdc3609ac71535b849775812455e4c40aedfbf0778a6f123b1774/setproctitle-1.3.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ad1c2c2baaba62823a7f348f469a967ece0062140ca39e7a48e4bbb1f20d54c4", size = 30625 }, - { url = "https://files.pythonhosted.org/packages/5f/73/a2a8259ebee166aee1ca53eead75de0e190b3ddca4f716e5c7470ebb7ef6/setproctitle-1.3.6-cp311-cp311-win32.whl", hash = "sha256:8050c01331135f77ec99d99307bfbc6519ea24d2f92964b06f3222a804a3ff1f", size = 11488 }, - { url = "https://files.pythonhosted.org/packages/c9/15/52cf5e1ff0727d53704cfdde2858eaf237ce523b0b04db65faa84ff83e13/setproctitle-1.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:9b73cf0fe28009a04a35bb2522e4c5b5176cc148919431dcb73fdbdfaab15781", size = 12201 }, - { url = "https://files.pythonhosted.org/packages/8f/fb/99456fd94d4207c5f6c40746a048a33a52b4239cd7d9c8d4889e2210ec82/setproctitle-1.3.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af44bb7a1af163806bbb679eb8432fa7b4fb6d83a5d403b541b675dcd3798638", size = 17399 }, - { url = "https://files.pythonhosted.org/packages/d5/48/9699191fe6062827683c43bfa9caac33a2c89f8781dd8c7253fa3dba85fd/setproctitle-1.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cca16fd055316a48f0debfcbfb6af7cea715429fc31515ab3fcac05abd527d8", size = 11966 }, - { url = "https://files.pythonhosted.org/packages/33/03/b085d192b9ecb9c7ce6ad6ef30ecf4110b7f39430b58a56245569827fcf4/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea002088d5554fd75e619742cefc78b84a212ba21632e59931b3501f0cfc8f67", size = 32017 }, - { url = "https://files.pythonhosted.org/packages/ae/68/c53162e645816f97212002111420d1b2f75bf6d02632e37e961dc2cd6d8b/setproctitle-1.3.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb465dd5825356c1191a038a86ee1b8166e3562d6e8add95eec04ab484cfb8a2", size = 33419 }, - { url = "https://files.pythonhosted.org/packages/ac/0d/119a45d15a816a6cf5ccc61b19729f82620095b27a47e0a6838216a95fae/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2c8e20487b3b73c1fa72c56f5c89430617296cd380373e7af3a538a82d4cd6d", size = 30711 }, - { url = "https://files.pythonhosted.org/packages/e3/fb/5e9b5068df9e9f31a722a775a5e8322a29a638eaaa3eac5ea7f0b35e6314/setproctitle-1.3.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d6252098e98129a1decb59b46920d4eca17b0395f3d71b0d327d086fefe77d", size = 31742 }, - { url = "https://files.pythonhosted.org/packages/35/88/54de1e73e8fce87d587889c7eedb48fc4ee2bbe4e4ca6331690d03024f86/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf355fbf0d4275d86f9f57be705d8e5eaa7f8ddb12b24ced2ea6cbd68fdb14dc", size = 31925 }, - { url = "https://files.pythonhosted.org/packages/f3/01/65948d7badd66e63e3db247b923143da142790fa293830fdecf832712c2d/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e288f8a162d663916060beb5e8165a8551312b08efee9cf68302687471a6545d", size = 30981 }, - { url = "https://files.pythonhosted.org/packages/22/20/c495e61786f1d38d5dc340b9d9077fee9be3dfc7e89f515afe12e1526dbc/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b2e54f4a2dc6edf0f5ea5b1d0a608d2af3dcb5aa8c8eeab9c8841b23e1b054fe", size = 33209 }, - { url = "https://files.pythonhosted.org/packages/98/3f/a457b8550fbd34d5b482fe20b8376b529e76bf1fbf9a474a6d9a641ab4ad/setproctitle-1.3.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b6f4abde9a2946f57e8daaf1160b2351bcf64274ef539e6675c1d945dbd75e2a", size = 31587 }, - { url = "https://files.pythonhosted.org/packages/44/fe/743517340e5a635e3f1c4310baea20c16c66202f96a6f4cead222ffd6d84/setproctitle-1.3.6-cp312-cp312-win32.whl", hash = "sha256:db608db98ccc21248370d30044a60843b3f0f3d34781ceeea67067c508cd5a28", size = 11487 }, - { url = "https://files.pythonhosted.org/packages/60/9a/d88f1c1f0f4efff1bd29d9233583ee341114dda7d9613941453984849674/setproctitle-1.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:082413db8a96b1f021088e8ec23f0a61fec352e649aba20881895815388b66d3", size = 12208 }, -] - [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] @@ -5146,116 +5207,125 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422, upload-time = "2025-05-19T11:04:41.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368 }, - { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362 }, - { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005 }, - { url = "https://files.pythonhosted.org/packages/a2/17/e09357274699c6e012bbb5a8ea14765a4d5860bb658df1931c9f90d53bd3/shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753", size = 3108489 }, - { url = "https://files.pythonhosted.org/packages/17/5d/93a6c37c4b4e9955ad40834f42b17260ca74ecf36df2e81bb14d12221b90/shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647", size = 3945727 }, - { url = "https://files.pythonhosted.org/packages/a3/1a/ad696648f16fd82dd6bfcca0b3b8fbafa7aacc13431c7fc4c9b49e481681/shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0", size = 4109311 }, - { url = "https://files.pythonhosted.org/packages/d4/38/150dd245beab179ec0d4472bf6799bf18f21b1efbef59ac87de3377dbf1c/shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab", size = 1522982 }, - { url = "https://files.pythonhosted.org/packages/93/5b/842022c00fbb051083c1c85430f3bb55565b7fd2d775f4f398c0ba8052ce/shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93", size = 1703872 }, - { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021 }, - { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018 }, - { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417 }, - { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224 }, - { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982 }, - { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122 }, - { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437 }, - { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479 }, + { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368, upload-time = "2025-05-19T11:03:55.937Z" }, + { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362, upload-time = "2025-05-19T11:03:57.06Z" }, + { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005, upload-time = "2025-05-19T11:03:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/a2/17/e09357274699c6e012bbb5a8ea14765a4d5860bb658df1931c9f90d53bd3/shapely-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586f6aee1edec04e16227517a866df3e9a2e43c1f635efc32978bb3dc9c63753", size = 3108489, upload-time = "2025-05-19T11:04:00.059Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/93a6c37c4b4e9955ad40834f42b17260ca74ecf36df2e81bb14d12221b90/shapely-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9878b9e37ad26c72aada8de0c9cfe418d9e2ff36992a1693b7f65a075b28647", size = 3945727, upload-time = "2025-05-19T11:04:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1a/ad696648f16fd82dd6bfcca0b3b8fbafa7aacc13431c7fc4c9b49e481681/shapely-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9a531c48f289ba355e37b134e98e28c557ff13965d4653a5228d0f42a09aed0", size = 4109311, upload-time = "2025-05-19T11:04:03.134Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/150dd245beab179ec0d4472bf6799bf18f21b1efbef59ac87de3377dbf1c/shapely-2.1.1-cp311-cp311-win32.whl", hash = "sha256:4866de2673a971820c75c0167b1f1cd8fb76f2d641101c23d3ca021ad0449bab", size = 1522982, upload-time = "2025-05-19T11:04:05.217Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/842022c00fbb051083c1c85430f3bb55565b7fd2d775f4f398c0ba8052ce/shapely-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:20a9d79958b3d6c70d8a886b250047ea32ff40489d7abb47d01498c704557a93", size = 1703872, upload-time = "2025-05-19T11:04:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021, upload-time = "2025-05-19T11:04:08.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018, upload-time = "2025-05-19T11:04:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417, upload-time = "2025-05-19T11:04:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224, upload-time = "2025-05-19T11:04:11.903Z" }, + { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982, upload-time = "2025-05-19T11:04:13.224Z" }, + { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122, upload-time = "2025-05-19T11:04:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437, upload-time = "2025-05-19T11:04:16.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479, upload-time = "2025-05-19T11:04:18.497Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "socksio" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 }, + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] [[package]] name = "sqlalchemy" -version = "2.0.35" +version = "2.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/48/4f190a83525f5cefefa44f6adc9e6386c4de5218d686c27eda92eb1f5424/sqlalchemy-2.0.35.tar.gz", hash = "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f", size = 9562798 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/46/9215a35bf98c3a2528e987791e6180eb51624d2c7d5cb8e2d96a6450b657/SQLAlchemy-2.0.35-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60", size = 2091274 }, - { url = "https://files.pythonhosted.org/packages/1e/69/919673c5101a0c633658d58b11b454b251ca82300941fba801201434755d/SQLAlchemy-2.0.35-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62", size = 2081672 }, - { url = "https://files.pythonhosted.org/packages/67/ea/a6b0597cbda12796be2302153369dbbe90573fdab3bc4885f8efac499247/SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6", size = 3200083 }, - { url = "https://files.pythonhosted.org/packages/8c/d6/97bdc8d714fb21762f2092511f380f18cdb2d985d516071fa925bb433a90/SQLAlchemy-2.0.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7", size = 3200080 }, - { url = "https://files.pythonhosted.org/packages/87/d2/8c2adaf2ade4f6f1b725acd0b0be9210bb6a2df41024729a8eec6a86fe5a/SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71", size = 3137108 }, - { url = "https://files.pythonhosted.org/packages/7e/ae/ea05d0bfa8f2b25ae34591895147152854fc950f491c4ce362ae06035db8/SQLAlchemy-2.0.35-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01", size = 3157437 }, - { url = "https://files.pythonhosted.org/packages/fe/5d/8ad6df01398388a766163d27960b3365f1bbd8bb7b05b5cad321a8b69b25/SQLAlchemy-2.0.35-cp311-cp311-win32.whl", hash = "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e", size = 2061935 }, - { url = "https://files.pythonhosted.org/packages/ff/68/8557efc0c32c8e2c147cb6512237448b8ed594a57cd015fda67f8e56bb3f/SQLAlchemy-2.0.35-cp311-cp311-win_amd64.whl", hash = "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8", size = 2087281 }, - { url = "https://files.pythonhosted.org/packages/2f/2b/fff87e6db0da31212c98bbc445f83fb608ea92b96bda3f3f10e373bac76c/SQLAlchemy-2.0.35-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2", size = 2089790 }, - { url = "https://files.pythonhosted.org/packages/68/92/4bb761bd82764d5827bf6b6095168c40fb5dbbd23670203aef2f96ba6bc6/SQLAlchemy-2.0.35-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468", size = 2080266 }, - { url = "https://files.pythonhosted.org/packages/22/46/068a65db6dc253c6f25a7598d99e0a1d60b14f661f9d09ef6c73c718fa4e/SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d", size = 3229760 }, - { url = "https://files.pythonhosted.org/packages/6e/36/59830dafe40dda592304debd4cd86e583f63472f3a62c9e2695a5795e786/SQLAlchemy-2.0.35-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db", size = 3240649 }, - { url = "https://files.pythonhosted.org/packages/00/50/844c50c6996f9c7f000c959dd1a7436a6c94e449ee113046a1d19e470089/SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c", size = 3176138 }, - { url = "https://files.pythonhosted.org/packages/df/d2/336b18cac68eecb67de474fc15c85f13be4e615c6f5bae87ea38c6734ce0/SQLAlchemy-2.0.35-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8", size = 3202753 }, - { url = "https://files.pythonhosted.org/packages/f0/f3/ee1e62fabdc10910b5ef720ae08e59bc785f26652876af3a50b89b97b412/SQLAlchemy-2.0.35-cp312-cp312-win32.whl", hash = "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf", size = 2060113 }, - { url = "https://files.pythonhosted.org/packages/60/63/a3cef44a52979169d884f3583d0640e64b3c28122c096474a1d7cfcaf1f3/SQLAlchemy-2.0.35-cp312-cp312-win_amd64.whl", hash = "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc", size = 2085839 }, - { url = "https://files.pythonhosted.org/packages/0e/c6/33c706449cdd92b1b6d756b247761e27d32230fd6b2de5f44c4c3e5632b2/SQLAlchemy-2.0.35-py3-none-any.whl", hash = "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1", size = 1881276 }, -] - -[[package]] -name = "sqlglot" -version = "26.24.0" +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "sseclient-py" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f7/0fa9f9f2477c4e3d8e28b0f5e066f0e72343c29c8a302ee6a77579e8986b/sqlglot-26.24.0.tar.gz", hash = "sha256:e778ca9cb685b4fc34b59d50432c20f463c63ec90d0448fa91afa7f320a88518", size = 5371208 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/ed/3df5ab8bb0c12f86c28d0cadb11ed1de44a92ed35ce7ff4fd5518a809325/sseclient-py-1.8.0.tar.gz", hash = "sha256:c547c5c1a7633230a38dc599a21a2dc638f9b5c297286b48b46b935c71fac3e8", size = 7791, upload-time = "2023-09-01T19:39:20.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/11/6995759d913d714ff443478f5865b2616dbcd32b12764d02df9550d7a61e/sqlglot-26.24.0-py3-none-any.whl", hash = "sha256:81f7e47bb1b4b396c564359f47c7c1aee476575a0cadf84dc35f7189cab87f82", size = 464043 }, + { url = "https://files.pythonhosted.org/packages/49/58/97655efdfeb5b4eeab85b1fc5d3fa1023661246c2ab2a26ea8e47402d4f2/sseclient_py-1.8.0-py2.py3-none-any.whl", hash = "sha256:4ecca6dc0b9f963f8384e9d7fd529bf93dd7d708144c4fb5da0e0a1a926fee83", size = 8828, upload-time = "2023-09-01T19:39:17.627Z" }, ] [[package]] @@ -5265,9 +5335,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/53/c3a36690a923706e7ac841f649c64f5108889ab1ec44218dac45771f252a/starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a", size = 2573755 } +sdist = { url = "https://files.pythonhosted.org/packages/78/53/c3a36690a923706e7ac841f649c64f5108889ab1ec44218dac45771f252a/starlette-0.41.0.tar.gz", hash = "sha256:39cbd8768b107d68bfe1ff1672b38a2c38b49777de46d2a592841d58e3bf7c2a", size = 2573755, upload-time = "2024-10-15T17:32:04.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/c6/a4443bfabf5629129512ca0e07866c4c3c094079ba4e9b2551006927253c/starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a", size = 73216 }, + { url = "https://files.pythonhosted.org/packages/35/c6/a4443bfabf5629129512ca0e07866c4c3c094079ba4e9b2551006927253c/starlette-0.41.0-py3-none-any.whl", hash = "sha256:a0193a3c413ebc9c78bff1c3546a45bb8c8bcb4a84cae8747d650a65bd37210a", size = 73216, upload-time = "2024-10-15T17:32:02.931Z" }, ] [[package]] @@ -5279,9 +5349,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/af/94cd4925c8a80b4c06bdef60226c04566973f6e2982957d2eabeecb2d5ca/storage3-0.8.2.tar.gz", hash = "sha256:db05d3fe8fb73bd30c814c4c4749664f37a5dfc78b629e8c058ef558c2b89f5a", size = 9041 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/af/94cd4925c8a80b4c06bdef60226c04566973f6e2982957d2eabeecb2d5ca/storage3-0.8.2.tar.gz", hash = "sha256:db05d3fe8fb73bd30c814c4c4749664f37a5dfc78b629e8c058ef558c2b89f5a", size = 9041, upload-time = "2024-10-18T07:05:40.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/67/7d281ba69b3ba3359f528bb0a1cac9d87896938d80119451123e829b3820/storage3-0.8.2-py3-none-any.whl", hash = "sha256:f2e995b18c77a2a9265d1a33047d43e4d6abb11eb3ca5067959f68281c305de3", size = 16230 }, + { url = "https://files.pythonhosted.org/packages/c8/67/7d281ba69b3ba3359f528bb0a1cac9d87896938d80119451123e829b3820/storage3-0.8.2-py3-none-any.whl", hash = "sha256:f2e995b18c77a2a9265d1a33047d43e4d6abb11eb3ca5067959f68281c305de3", size = 16230, upload-time = "2024-10-18T07:05:38.408Z" }, ] [[package]] @@ -5297,9 +5367,9 @@ dependencies = [ { name = "supafunc" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/46/0846eae977d7e067e73960d880a3457e2a87b1ec7467ff3bc5365b318df7/supabase-2.8.1.tar.gz", hash = "sha256:711c70e6acd9e2ff48ca0dc0b1bb70c01c25378cc5189ec9f5ed9655b30bc41d", size = 13955 } +sdist = { url = "https://files.pythonhosted.org/packages/80/46/0846eae977d7e067e73960d880a3457e2a87b1ec7467ff3bc5365b318df7/supabase-2.8.1.tar.gz", hash = "sha256:711c70e6acd9e2ff48ca0dc0b1bb70c01c25378cc5189ec9f5ed9655b30bc41d", size = 13955, upload-time = "2024-09-30T16:03:53.548Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/ca/7f1dfcd9dfff2cb56ce063b3c8e4c29ae43e50102f039d5196cbed8d51b8/supabase-2.8.1-py3-none-any.whl", hash = "sha256:dfa8bef89b54129093521d5bba2136ff765baf67cd76d8ad0aa4984d61a7815c", size = 16589 }, + { url = "https://files.pythonhosted.org/packages/15/ca/7f1dfcd9dfff2cb56ce063b3c8e4c29ae43e50102f039d5196cbed8d51b8/supabase-2.8.1-py3-none-any.whl", hash = "sha256:dfa8bef89b54129093521d5bba2136ff765baf67cd76d8ad0aa4984d61a7815c", size = 16589, upload-time = "2024-09-30T16:03:51.737Z" }, ] [[package]] @@ -5309,9 +5379,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", extra = ["http2"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/28/c808bfd80c996cbf0ba5de6714edf2e2f68637f50058f6b9373f49b82a70/supafunc-0.6.2.tar.gz", hash = "sha256:c7dfa20db7182f7fe4ae436e94e05c06cd7ed98d697fed75d68c7b9792822adc", size = 3902 } +sdist = { url = "https://files.pythonhosted.org/packages/85/28/c808bfd80c996cbf0ba5de6714edf2e2f68637f50058f6b9373f49b82a70/supafunc-0.6.2.tar.gz", hash = "sha256:c7dfa20db7182f7fe4ae436e94e05c06cd7ed98d697fed75d68c7b9792822adc", size = 3902, upload-time = "2024-10-18T07:06:39.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/91/cb7a31cf250ee66dfd40cca2c7c36eede7e1d8e3183f99865d14438c66a7/supafunc-0.6.2-py3-none-any.whl", hash = "sha256:101b30616b0a1ce8cf938eca1df362fa4cf1deacb0271f53ebbd674190fb0da5", size = 6622 }, + { url = "https://files.pythonhosted.org/packages/18/91/cb7a31cf250ee66dfd40cca2c7c36eede7e1d8e3183f99865d14438c66a7/supafunc-0.6.2-py3-none-any.whl", hash = "sha256:101b30616b0a1ce8cf938eca1df362fa4cf1deacb0271f53ebbd674190fb0da5", size = 6622, upload-time = "2024-10-18T07:06:37.782Z" }, ] [[package]] @@ -5321,19 +5391,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "tablestore" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "crc32c" }, - { name = "enum34" }, { name = "flatbuffers" }, { name = "future" }, { name = "numpy" }, @@ -5341,15 +5410,18 @@ dependencies = [ { name = "six" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/ed/5bdd906ec9d2dbae3909525dbb7602558c377e0cbcdddb6405d2d0d3f1af/tablestore-6.1.0.tar.gz", hash = "sha256:bfe6a3e0fe88a230729723c357f4a46b8869a06a4b936db20692ed587a721c1c", size = 135690 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/58/48d65d181a69f7db19f7cdee01d252168fbfbad2d1bb25abed03e6df3b05/tablestore-6.2.0.tar.gz", hash = "sha256:0773e77c00542be1bfebbc3c7a85f72a881c63e4e7df7c5a9793a54144590e68", size = 85942, upload-time = "2025-04-15T12:11:20.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/da/30451712a769bcf417add8e81163d478a4d668b0e8d489a9d667260d55df/tablestore-6.2.0-py3-none-any.whl", hash = "sha256:6af496d841ab1ff3f78b46abbd87b95a08d89605c51664d2b30933b1d1c5583a", size = 106297, upload-time = "2025-04-15T12:11:17.476Z" }, +] [[package]] name = "tabulate" version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] [[package]] @@ -5362,9 +5434,9 @@ dependencies = [ { name = "numpy" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/3f/9487f703edb5b8be51ada52b675b4b2fcd507399946aeab8c10028f75265/tcvdb_text-1.1.1.tar.gz", hash = "sha256:db36b5d7b640b194ae72c0c429718c9613b8ef9de5fffb9d510aba5be75ff1cb", size = 57859792 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/3f/9487f703edb5b8be51ada52b675b4b2fcd507399946aeab8c10028f75265/tcvdb_text-1.1.1.tar.gz", hash = "sha256:db36b5d7b640b194ae72c0c429718c9613b8ef9de5fffb9d510aba5be75ff1cb", size = 57859792, upload-time = "2025-02-07T11:08:17.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/d3/8c8799802676bc6c4696bed7ca7b01a3a5b6ab080ed959e5a4925640e01b/tcvdb_text-1.1.1-py3-none-any.whl", hash = "sha256:981eb2323c0668129942c066de05e8f0d2165be36f567877906646dea07d17a9", size = 59535083 }, + { url = "https://files.pythonhosted.org/packages/76/d3/8c8799802676bc6c4696bed7ca7b01a3a5b6ab080ed959e5a4925640e01b/tcvdb_text-1.1.1-py3-none-any.whl", hash = "sha256:981eb2323c0668129942c066de05e8f0d2165be36f567877906646dea07d17a9", size = 59535083, upload-time = "2025-02-07T11:07:59.66Z" }, ] [[package]] @@ -5382,18 +5454,18 @@ dependencies = [ { name = "ujson" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/ec/c80579aff1539257aafcf8dc3f3c13630171f299d65b33b68440e166f27c/tcvectordb-1.6.4.tar.gz", hash = "sha256:6fb18e15ccc6744d5147e9bbd781f84df3d66112de7d9cc615878b3f72d3a29a", size = 75188 } +sdist = { url = "https://files.pythonhosted.org/packages/19/ec/c80579aff1539257aafcf8dc3f3c13630171f299d65b33b68440e166f27c/tcvectordb-1.6.4.tar.gz", hash = "sha256:6fb18e15ccc6744d5147e9bbd781f84df3d66112de7d9cc615878b3f72d3a29a", size = 75188, upload-time = "2025-03-05T09:14:19.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/bf/f38d9f629324ecffca8fe934e8df47e1233a9021b0739447e59e9fb248f9/tcvectordb-1.6.4-py3-none-any.whl", hash = "sha256:06ef13e7edb4575b04615065fc90e1a28374e318ada305f3786629aec5c9318a", size = 88917 }, + { url = "https://files.pythonhosted.org/packages/68/bf/f38d9f629324ecffca8fe934e8df47e1233a9021b0739447e59e9fb248f9/tcvectordb-1.6.4-py3-none-any.whl", hash = "sha256:06ef13e7edb4575b04615065fc90e1a28374e318ada305f3786629aec5c9318a", size = 88917, upload-time = "2025-03-05T09:14:17.494Z" }, ] [[package]] name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] [[package]] @@ -5403,9 +5475,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/98/ab324fdfbbf064186ca621e21aa3871ddf886ecb78358a9864509241e802/tidb_vector-0.0.9.tar.gz", hash = "sha256:e10680872532808e1bcffa7a92dd2b05bb65d63982f833edb3c6cd590dec7709", size = 16948 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/98/ab324fdfbbf064186ca621e21aa3871ddf886ecb78358a9864509241e802/tidb_vector-0.0.9.tar.gz", hash = "sha256:e10680872532808e1bcffa7a92dd2b05bb65d63982f833edb3c6cd590dec7709", size = 16948, upload-time = "2024-05-08T07:54:36.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/bb/0f3b7b4d31537e90f4dd01f50fa58daef48807c789c1c1bdd610204ff103/tidb_vector-0.0.9-py3-none-any.whl", hash = "sha256:db060ee1c981326d3882d0810e0b8b57811f278668f9381168997b360c4296c2", size = 17026 }, + { url = "https://files.pythonhosted.org/packages/5d/bb/0f3b7b4d31537e90f4dd01f50fa58daef48807c789c1c1bdd610204ff103/tidb_vector-0.0.9-py3-none-any.whl", hash = "sha256:db060ee1c981326d3882d0810e0b8b57811f278668f9381168997b360c4296c2", size = 17026, upload-time = "2024-05-08T07:54:34.849Z" }, ] [[package]] @@ -5416,83 +5488,83 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987 }, - { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155 }, - { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898 }, - { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535 }, - { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548 }, - { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895 }, - { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 }, - { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 }, - { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 }, - { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 }, - { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 }, - { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 }, + { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987, upload-time = "2025-02-14T06:02:14.174Z" }, + { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155, upload-time = "2025-02-14T06:02:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898, upload-time = "2025-02-14T06:02:16.666Z" }, + { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535, upload-time = "2025-02-14T06:02:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548, upload-time = "2025-02-14T06:02:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895, upload-time = "2025-02-14T06:02:22.67Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, ] [[package]] name = "tokenizers" -version = "0.21.1" +version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767 }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555 }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541 }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058 }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278 }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253 }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225 }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874 }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448 }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877 }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645 }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380 }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506 }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481 }, + { url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" }, + { url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" }, + { url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" }, + { url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] @@ -5506,7 +5578,7 @@ dependencies = [ { name = "requests" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/01/f811af86f1f80d5f289be075c3b281e74bf3fe081cfbe5cfce44954d2c3a/tos-2.7.2.tar.gz", hash = "sha256:3c31257716785bca7b2cac51474ff32543cda94075a7b7aff70d769c15c7b7ed", size = 123407 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/01/f811af86f1f80d5f289be075c3b281e74bf3fe081cfbe5cfce44954d2c3a/tos-2.7.2.tar.gz", hash = "sha256:3c31257716785bca7b2cac51474ff32543cda94075a7b7aff70d769c15c7b7ed", size = 123407, upload-time = "2024-10-16T15:59:08.634Z" } [[package]] name = "tqdm" @@ -5515,9 +5587,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] @@ -5536,9 +5608,9 @@ dependencies = [ { name = "tokenizers" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, ] [[package]] @@ -5551,27 +5623,27 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, ] [[package]] name = "types-aiofiles" -version = "24.1.0.20250516" +version = "24.1.0.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/13/41faafda1d85fc8afa744ee67a2d788b3ba63e5f1c7303e5d10c2d784d2d/types_aiofiles-24.1.0.20250516.tar.gz", hash = "sha256:7fd2a7f793bbe180b7b22cd4f59300fe61fdc9940b3bbc9899ffe32849b95188", size = 14304 } +sdist = { url = "https://files.pythonhosted.org/packages/4a/d6/5c44761bc11cb5c7505013a39f397a9016bfb3a5c932032b2db16c38b87b/types_aiofiles-24.1.0.20250708.tar.gz", hash = "sha256:c8207ed7385491ce5ba94da02658164ebd66b69a44e892288c9f20cbbf5284ff", size = 14322, upload-time = "2025-07-08T03:14:44.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/e7/828287ba5d1107db1093a123fe8e481eb5aab55c911f1100f28d0df80d5a/types_aiofiles-24.1.0.20250516-py3-none-any.whl", hash = "sha256:ec265994629146804b656a971c46f393ce860305834b3cacb4b8b6fb7dba7e33", size = 14253 }, + { url = "https://files.pythonhosted.org/packages/44/e9/4e0cc79c630040aae0634ac9393341dc2aff1a5be454be9741cc6cc8989f/types_aiofiles-24.1.0.20250708-py3-none-any.whl", hash = "sha256:07f8f06465fd415d9293467d1c66cd074b2c3b62b679e26e353e560a8cf63720", size = 14320, upload-time = "2025-07-08T03:14:44.009Z" }, ] [[package]] name = "types-awscrt" -version = "0.27.2" +version = "0.27.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/6c/583522cfb3c330e92e726af517a91c13247e555e021791a60f1b03c6ff16/types_awscrt-0.27.2.tar.gz", hash = "sha256:acd04f57119eb15626ab0ba9157fc24672421de56e7bd7b9f61681fedee44e91", size = 16304 } +sdist = { url = "https://files.pythonhosted.org/packages/94/95/02564024f8668feab6733a2c491005b5281b048b3d0573510622cbcd9fd4/types_awscrt-0.27.4.tar.gz", hash = "sha256:c019ba91a097e8a31d6948f6176ede1312963f41cdcacf82482ac877cbbcf390", size = 16941, upload-time = "2025-06-29T22:58:04.756Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/82/1ee2e5c9d28deac086ab3a6ff07c8bc393ef013a083f546c623699881715/types_awscrt-0.27.2-py3-none-any.whl", hash = "sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e", size = 37761 }, + { url = "https://files.pythonhosted.org/packages/d4/40/cb4d04df4ac3520858f5b397a4ab89f34be2601000002a26edd8ddc0cac5/types_awscrt-0.27.4-py3-none-any.whl", hash = "sha256:a8c4b9d9ae66d616755c322aba75ab9bd793c6fef448917e6de2e8b8cdf66fb4", size = 39626, upload-time = "2025-06-29T22:58:03.157Z" }, ] [[package]] @@ -5581,18 +5653,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-html5lib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628, upload-time = "2025-05-16T03:09:09.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879 }, + { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879, upload-time = "2025-05-16T03:09:09.051Z" }, ] [[package]] name = "types-cachetools" version = "5.5.0.20240820" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/7e/ad6ba4a56b2a994e0f0a04a61a50466b60ee88a13d10a18c83ac14a66c61/types-cachetools-5.5.0.20240820.tar.gz", hash = "sha256:b888ab5c1a48116f7799cd5004b18474cd82b5463acb5ffb2db2fc9c7b053bc0", size = 4198 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/7e/ad6ba4a56b2a994e0f0a04a61a50466b60ee88a13d10a18c83ac14a66c61/types-cachetools-5.5.0.20240820.tar.gz", hash = "sha256:b888ab5c1a48116f7799cd5004b18474cd82b5463acb5ffb2db2fc9c7b053bc0", size = 4198, upload-time = "2024-08-20T02:30:07.525Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4d/fd7cc050e2d236d5570c4d92531c0396573a1e14b31735870e849351c717/types_cachetools-5.5.0.20240820-py3-none-any.whl", hash = "sha256:efb2ed8bf27a4b9d3ed70d33849f536362603a90b8090a328acf0cd42fda82e2", size = 4149 }, + { url = "https://files.pythonhosted.org/packages/27/4d/fd7cc050e2d236d5570c4d92531c0396573a1e14b31735870e849351c717/types_cachetools-5.5.0.20240820-py3-none-any.whl", hash = "sha256:efb2ed8bf27a4b9d3ed70d33849f536362603a90b8090a328acf0cd42fda82e2", size = 4149, upload-time = "2024-08-20T02:30:06.461Z" }, ] [[package]] @@ -5602,45 +5674,45 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/5f/ac80a2f55757019e5d4809d17544569c47a623565258ca1a836ba951d53f/types_cffi-1.17.0.20250523.tar.gz", hash = "sha256:e7110f314c65590533adae1b30763be08ca71ad856a1ae3fe9b9d8664d49ec22", size = 16858 } +sdist = { url = "https://files.pythonhosted.org/packages/f7/5f/ac80a2f55757019e5d4809d17544569c47a623565258ca1a836ba951d53f/types_cffi-1.17.0.20250523.tar.gz", hash = "sha256:e7110f314c65590533adae1b30763be08ca71ad856a1ae3fe9b9d8664d49ec22", size = 16858, upload-time = "2025-05-23T03:05:40.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/86/e26e6ae4dfcbf6031b8422c22cf3a9eb2b6d127770406e7645b6248d8091/types_cffi-1.17.0.20250523-py3-none-any.whl", hash = "sha256:e98c549d8e191f6220e440f9f14315d6775a21a0e588c32c20476be885b2fad9", size = 20010 }, + { url = "https://files.pythonhosted.org/packages/f1/86/e26e6ae4dfcbf6031b8422c22cf3a9eb2b6d127770406e7645b6248d8091/types_cffi-1.17.0.20250523-py3-none-any.whl", hash = "sha256:e98c549d8e191f6220e440f9f14315d6775a21a0e588c32c20476be885b2fad9", size = 20010, upload-time = "2025-05-23T03:05:39.136Z" }, ] [[package]] name = "types-colorama" version = "0.4.15.20240311" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/73/0fb0b9fe4964b45b2a06ed41b60c352752626db46aa0fb70a49a9e283a75/types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a", size = 5608 } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/0fb0b9fe4964b45b2a06ed41b60c352752626db46aa0fb70a49a9e283a75/types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a", size = 5608, upload-time = "2024-03-11T02:15:51.557Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/83/6944b4fa01efb2e63ac62b791a8ddf0fee358f93be9f64b8f152648ad9d3/types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e", size = 5840 }, + { url = "https://files.pythonhosted.org/packages/b7/83/6944b4fa01efb2e63ac62b791a8ddf0fee358f93be9f64b8f152648ad9d3/types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e", size = 5840, upload-time = "2024-03-11T02:15:50.43Z" }, ] [[package]] name = "types-defusedxml" -version = "0.7.0.20250516" +version = "0.7.0.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/9d/3ba8b80536402f1a125bc5a44d82ab686aafa55a85f56160e076b2ac30de/types_defusedxml-0.7.0.20250516.tar.gz", hash = "sha256:164c2945077fa450f24ed09633f8b3a80694687fefbbc1cba5f24e4ba570666b", size = 10298 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/4b/79d046a7211e110afd885be04bb9423546df2a662ed28251512d60e51fb6/types_defusedxml-0.7.0.20250708.tar.gz", hash = "sha256:7b785780cc11c18a1af086308bf94bf53a0907943a1d145dbe00189bef323cb8", size = 10541, upload-time = "2025-07-08T03:14:33.325Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/7b/567b0978150edccf7fa3aa8f2566ea9c3ffc9481ce7d64428166934d6d7f/types_defusedxml-0.7.0.20250516-py3-none-any.whl", hash = "sha256:00e793e5c385c3e142d7c2acc3b4ccea2fe0828cee11e35501f0ba40386630a0", size = 12576 }, + { url = "https://files.pythonhosted.org/packages/24/f8/870de7fbd5fee5643f05061db948df6bd574a05a42aee91e37ad47c999ef/types_defusedxml-0.7.0.20250708-py3-none-any.whl", hash = "sha256:cc426cbc31c61a0f1b1c2ad9b9ef9ef846645f28fd708cd7727a6353b5c52e54", size = 13478, upload-time = "2025-07-08T03:14:32.633Z" }, ] [[package]] name = "types-deprecated" version = "1.2.15.20250304" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/67/eeefaaabb03b288aad85483d410452c8bbcbf8b2bd876b0e467ebd97415b/types_deprecated-1.2.15.20250304.tar.gz", hash = "sha256:c329030553029de5cc6cb30f269c11f4e00e598c4241290179f63cda7d33f719", size = 8015 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/67/eeefaaabb03b288aad85483d410452c8bbcbf8b2bd876b0e467ebd97415b/types_deprecated-1.2.15.20250304.tar.gz", hash = "sha256:c329030553029de5cc6cb30f269c11f4e00e598c4241290179f63cda7d33f719", size = 8015, upload-time = "2025-03-04T02:48:17.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/e3/c18aa72ab84e0bc127a3a94e93be1a6ac2cb281371d3a45376ab7cfdd31c/types_deprecated-1.2.15.20250304-py3-none-any.whl", hash = "sha256:86a65aa550ea8acf49f27e226b8953288cd851de887970fbbdf2239c116c3107", size = 8553 }, + { url = "https://files.pythonhosted.org/packages/4d/e3/c18aa72ab84e0bc127a3a94e93be1a6ac2cb281371d3a45376ab7cfdd31c/types_deprecated-1.2.15.20250304-py3-none-any.whl", hash = "sha256:86a65aa550ea8acf49f27e226b8953288cd851de887970fbbdf2239c116c3107", size = 8553, upload-time = "2025-03-04T02:48:16.666Z" }, ] [[package]] name = "types-docutils" -version = "0.21.0.20250526" +version = "0.21.0.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/bf/bb5695f7a9660f79a9cd999ea13ff7331b8f2d03aec3d2fd7c38be4bc8aa/types_docutils-0.21.0.20250526.tar.gz", hash = "sha256:6c7ba387716315df0d86a796baec9d5a71825ed2746cb7763193aafbb70ac86c", size = 38140 } +sdist = { url = "https://files.pythonhosted.org/packages/39/86/24394a71a04f416ca03df51863a3d3e2cd0542fdc40989188dca30ffb5bf/types_docutils-0.21.0.20250708.tar.gz", hash = "sha256:5625a82a9a2f26d8384545607c157e023a48ed60d940dfc738db125282864172", size = 42011, upload-time = "2025-07-08T03:14:24.214Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/84/73bca8d1364f6685bd6e00eaa15e653ef96163231fbd7a612f3a845497fb/types_docutils-0.21.0.20250526-py3-none-any.whl", hash = "sha256:44d9f9ed19bb75071deb6804947c123f30bbc617a656420f044e09b9f16b72d1", size = 62000 }, + { url = "https://files.pythonhosted.org/packages/bd/17/8c1153fc1576a0dcffdd157c69a12863c3f9485054256f6791ea17d95aed/types_docutils-0.21.0.20250708-py3-none-any.whl", hash = "sha256:166630d1aec18b9ca02547873210e04bf7674ba8f8da9cd9e6a5e77dc99372c2", size = 67953, upload-time = "2025-07-08T03:14:23.057Z" }, ] [[package]] @@ -5650,9 +5722,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/f3/dd2f0d274ecb77772d3ce83735f75ad14713461e8cf7e6d61a7c272037b1/types_flask_cors-5.0.0.20250413.tar.gz", hash = "sha256:b346d052f4ef3b606b73faf13e868e458f1efdbfedcbe1aba739eb2f54a6cf5f", size = 9921 } +sdist = { url = "https://files.pythonhosted.org/packages/a4/f3/dd2f0d274ecb77772d3ce83735f75ad14713461e8cf7e6d61a7c272037b1/types_flask_cors-5.0.0.20250413.tar.gz", hash = "sha256:b346d052f4ef3b606b73faf13e868e458f1efdbfedcbe1aba739eb2f54a6cf5f", size = 9921, upload-time = "2025-04-13T04:04:15.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/34/7d64eb72d80bfd5b9e6dd31e7fe351a1c9a735f5c01e85b1d3b903a9d656/types_flask_cors-5.0.0.20250413-py3-none-any.whl", hash = "sha256:8183fdba764d45a5b40214468a1d5daa0e86c4ee6042d13f38cc428308f27a64", size = 9982 }, + { url = "https://files.pythonhosted.org/packages/66/34/7d64eb72d80bfd5b9e6dd31e7fe351a1c9a735f5c01e85b1d3b903a9d656/types_flask_cors-5.0.0.20250413-py3-none-any.whl", hash = "sha256:8183fdba764d45a5b40214468a1d5daa0e86c4ee6042d13f38cc428308f27a64", size = 9982, upload-time = "2025-04-13T04:04:14.27Z" }, ] [[package]] @@ -5663,9 +5735,9 @@ dependencies = [ { name = "flask" }, { name = "flask-sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/2a/15d922ddd3fad1ec0e06dab338f20c508becacaf8193ff373aee6986a1cc/types_flask_migrate-4.1.0.20250112.tar.gz", hash = "sha256:f2d2c966378ae7bb0660ec810e9af0a56ca03108235364c2a7b5e90418b0ff67", size = 8650 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/2a/15d922ddd3fad1ec0e06dab338f20c508becacaf8193ff373aee6986a1cc/types_flask_migrate-4.1.0.20250112.tar.gz", hash = "sha256:f2d2c966378ae7bb0660ec810e9af0a56ca03108235364c2a7b5e90418b0ff67", size = 8650, upload-time = "2025-01-12T02:51:25.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/01/56e26643c54c5101a7bc11d277d15cd871b05a8a3ddbcc9acd3634d7fff8/types_Flask_Migrate-4.1.0.20250112-py3-none-any.whl", hash = "sha256:1814fffc609c2ead784affd011de92f0beecd48044963a8c898dd107dc1b5969", size = 8727 }, + { url = "https://files.pythonhosted.org/packages/36/01/56e26643c54c5101a7bc11d277d15cd871b05a8a3ddbcc9acd3634d7fff8/types_Flask_Migrate-4.1.0.20250112-py3-none-any.whl", hash = "sha256:1814fffc609c2ead784affd011de92f0beecd48044963a8c898dd107dc1b5969", size = 8727, upload-time = "2025-01-12T02:51:23.121Z" }, ] [[package]] @@ -5676,36 +5748,36 @@ dependencies = [ { name = "types-greenlet" }, { name = "types-psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/db/bdade74c3ba3a266eafd625377eb7b9b37c9c724c7472192100baf0fe507/types_gevent-24.11.0.20250401.tar.gz", hash = "sha256:1443f796a442062698e67d818fca50aa88067dee4021d457a7c0c6bedd6f46ca", size = 36980 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/db/bdade74c3ba3a266eafd625377eb7b9b37c9c724c7472192100baf0fe507/types_gevent-24.11.0.20250401.tar.gz", hash = "sha256:1443f796a442062698e67d818fca50aa88067dee4021d457a7c0c6bedd6f46ca", size = 36980, upload-time = "2025-04-01T03:07:30.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/3d/c8b12d048565ef12ae65d71a0e566f36c6e076b158d3f94d87edddbeea6b/types_gevent-24.11.0.20250401-py3-none-any.whl", hash = "sha256:6764faf861ea99250c38179c58076392c44019ac3393029f71b06c4a15e8c1d1", size = 54863 }, + { url = "https://files.pythonhosted.org/packages/25/3d/c8b12d048565ef12ae65d71a0e566f36c6e076b158d3f94d87edddbeea6b/types_gevent-24.11.0.20250401-py3-none-any.whl", hash = "sha256:6764faf861ea99250c38179c58076392c44019ac3393029f71b06c4a15e8c1d1", size = 54863, upload-time = "2025-04-01T03:07:29.147Z" }, ] [[package]] name = "types-greenlet" version = "3.1.0.20250401" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/c9/50405ed194a02f02a418311311e6ee4dd73eed446608b679e6df8170d5b7/types_greenlet-3.1.0.20250401.tar.gz", hash = "sha256:949389b64c34ca9472f6335189e9fe0b2e9704436d4f0850e39e9b7145909082", size = 8460 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/c9/50405ed194a02f02a418311311e6ee4dd73eed446608b679e6df8170d5b7/types_greenlet-3.1.0.20250401.tar.gz", hash = "sha256:949389b64c34ca9472f6335189e9fe0b2e9704436d4f0850e39e9b7145909082", size = 8460, upload-time = "2025-04-01T03:06:44.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/f3/36c5a6db23761c810d91227146f20b6e501aa50a51a557bd14e021cd9aea/types_greenlet-3.1.0.20250401-py3-none-any.whl", hash = "sha256:77987f3249b0f21415dc0254057e1ae4125a696a9bba28b0bcb67ee9e3dc14f6", size = 8821 }, + { url = "https://files.pythonhosted.org/packages/a5/f3/36c5a6db23761c810d91227146f20b6e501aa50a51a557bd14e021cd9aea/types_greenlet-3.1.0.20250401-py3-none-any.whl", hash = "sha256:77987f3249b0f21415dc0254057e1ae4125a696a9bba28b0bcb67ee9e3dc14f6", size = 8821, upload-time = "2025-04-01T03:06:42.945Z" }, ] [[package]] name = "types-html5lib" -version = "1.1.11.20250516" +version = "1.1.11.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ed/9f092ff479e2b5598941855f314a22953bb04b5fb38bcba3f880feb833ba/types_html5lib-1.1.11.20250516.tar.gz", hash = "sha256:65043a6718c97f7d52567cc0cdf41efbfc33b1f92c6c0c5e19f60a7ec69ae720", size = 16136 } +sdist = { url = "https://files.pythonhosted.org/packages/d4/3b/1f5ba4358cfc1421cced5cdb9d2b08b4b99e4f9a41da88ce079f6d1a7bf1/types_html5lib-1.1.11.20250708.tar.gz", hash = "sha256:24321720fdbac71cee50d5a4bec9b7448495b7217974cffe3fcf1ede4eef7afe", size = 16799, upload-time = "2025-07-08T03:13:53.14Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/3b/cb5b23c7b51bf48b8c9f175abb9dce2f1ecd2d2c25f92ea9f4e3720e9398/types_html5lib-1.1.11.20250516-py3-none-any.whl", hash = "sha256:5e407b14b1bd2b9b1107cbd1e2e19d4a0c46d60febd231c7ab7313d7405663c1", size = 21770 }, + { url = "https://files.pythonhosted.org/packages/a8/50/5fc23cf647eee23acdd337c8150861d39980cf11f33dd87f78e87d2a4bad/types_html5lib-1.1.11.20250708-py3-none-any.whl", hash = "sha256:bb898066b155de7081cb182179e2ded31b9e0e234605e2cb46536894e68a6954", size = 22913, upload-time = "2025-07-08T03:13:52.098Z" }, ] [[package]] name = "types-jmespath" version = "1.0.2.20250529" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/ce/1083f6dcf5e7f25e9abcb67f870799d45f8b184cdb6fd23bbe541d17d9cc/types_jmespath-1.0.2.20250529.tar.gz", hash = "sha256:d3c08397f57fe0510e3b1b02c27f0a5e738729680fb0ea5f4b74f70fb032c129", size = 10138 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/ce/1083f6dcf5e7f25e9abcb67f870799d45f8b184cdb6fd23bbe541d17d9cc/types_jmespath-1.0.2.20250529.tar.gz", hash = "sha256:d3c08397f57fe0510e3b1b02c27f0a5e738729680fb0ea5f4b74f70fb032c129", size = 10138, upload-time = "2025-05-29T03:07:30.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/74/78c518aeb310cc809aaf1dd19e646f8d42c472344a720b39e1ba2a65c2e7/types_jmespath-1.0.2.20250529-py3-none-any.whl", hash = "sha256:6344c102233aae954d623d285618079d797884e35f6cd8d2a894ca02640eca07", size = 11409 }, + { url = "https://files.pythonhosted.org/packages/66/74/78c518aeb310cc809aaf1dd19e646f8d42c472344a720b39e1ba2a65c2e7/types_jmespath-1.0.2.20250529-py3-none-any.whl", hash = "sha256:6344c102233aae954d623d285618079d797884e35f6cd8d2a894ca02640eca07", size = 11409, upload-time = "2025-05-29T03:07:29.012Z" }, ] [[package]] @@ -5715,90 +5787,90 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/27ea5bffdb306bf261f6677a98b6993d93893b2c2e30f7ecc1d2c99d32e7/types_jsonschema-4.23.0.20250516.tar.gz", hash = "sha256:9ace09d9d35c4390a7251ccd7d833b92ccc189d24d1b347f26212afce361117e", size = 14911 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/27ea5bffdb306bf261f6677a98b6993d93893b2c2e30f7ecc1d2c99d32e7/types_jsonschema-4.23.0.20250516.tar.gz", hash = "sha256:9ace09d9d35c4390a7251ccd7d833b92ccc189d24d1b347f26212afce361117e", size = 14911, upload-time = "2025-05-16T03:09:33.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/48/73ae8b388e19fc4a2a8060d0876325ec7310cfd09b53a2185186fd35959f/types_jsonschema-4.23.0.20250516-py3-none-any.whl", hash = "sha256:e7d0dd7db7e59e63c26e3230e26ffc64c4704cc5170dc21270b366a35ead1618", size = 15027 }, + { url = "https://files.pythonhosted.org/packages/e6/48/73ae8b388e19fc4a2a8060d0876325ec7310cfd09b53a2185186fd35959f/types_jsonschema-4.23.0.20250516-py3-none-any.whl", hash = "sha256:e7d0dd7db7e59e63c26e3230e26ffc64c4704cc5170dc21270b366a35ead1618", size = 15027, upload-time = "2025-05-16T03:09:32.499Z" }, ] [[package]] name = "types-markdown" version = "3.7.0.20250322" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/fd/b4bd01b8c46f021c35a07aa31fe1dc45d21adc9fc8d53064bfa577aae73d/types_markdown-3.7.0.20250322.tar.gz", hash = "sha256:a48ed82dfcb6954592a10f104689d2d44df9125ce51b3cee20e0198a5216d55c", size = 18052 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/fd/b4bd01b8c46f021c35a07aa31fe1dc45d21adc9fc8d53064bfa577aae73d/types_markdown-3.7.0.20250322.tar.gz", hash = "sha256:a48ed82dfcb6954592a10f104689d2d44df9125ce51b3cee20e0198a5216d55c", size = 18052, upload-time = "2025-03-22T02:48:46.193Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/59/ee46617bc2b5e43bc06a000fdcd6358a013957e30ad545bed5e3456a4341/types_markdown-3.7.0.20250322-py3-none-any.whl", hash = "sha256:7e855503027b4290355a310fb834871940d9713da7c111f3e98a5e1cbc77acfb", size = 23699 }, + { url = "https://files.pythonhosted.org/packages/56/59/ee46617bc2b5e43bc06a000fdcd6358a013957e30ad545bed5e3456a4341/types_markdown-3.7.0.20250322-py3-none-any.whl", hash = "sha256:7e855503027b4290355a310fb834871940d9713da7c111f3e98a5e1cbc77acfb", size = 23699, upload-time = "2025-03-22T02:48:45.001Z" }, ] [[package]] name = "types-oauthlib" version = "3.2.0.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/2c/dba2c193ccff2d1e2835589d4075b230d5627b9db363e9c8de153261d6ec/types_oauthlib-3.2.0.20250516.tar.gz", hash = "sha256:56bf2cffdb8443ae718d4e83008e3fbd5f861230b4774e6d7799527758119d9a", size = 24683 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/2c/dba2c193ccff2d1e2835589d4075b230d5627b9db363e9c8de153261d6ec/types_oauthlib-3.2.0.20250516.tar.gz", hash = "sha256:56bf2cffdb8443ae718d4e83008e3fbd5f861230b4774e6d7799527758119d9a", size = 24683, upload-time = "2025-05-16T03:07:42.484Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/54/cdd62283338616fd2448f534b29110d79a42aaabffaf5f45e7aed365a366/types_oauthlib-3.2.0.20250516-py3-none-any.whl", hash = "sha256:5799235528bc9bd262827149a1633ff55ae6e5a5f5f151f4dae74359783a31b3", size = 45671 }, + { url = "https://files.pythonhosted.org/packages/b8/54/cdd62283338616fd2448f534b29110d79a42aaabffaf5f45e7aed365a366/types_oauthlib-3.2.0.20250516-py3-none-any.whl", hash = "sha256:5799235528bc9bd262827149a1633ff55ae6e5a5f5f151f4dae74359783a31b3", size = 45671, upload-time = "2025-05-16T03:07:41.268Z" }, ] [[package]] name = "types-objgraph" version = "3.6.0.20240907" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/48/ba0ec63d392904eee34ef1cbde2d8798f79a3663950e42fbbc25fd1bd6f7/types-objgraph-3.6.0.20240907.tar.gz", hash = "sha256:2e3dee675843ae387889731550b0ddfed06e9420946cf78a4bca565b5fc53634", size = 2928 } +sdist = { url = "https://files.pythonhosted.org/packages/22/48/ba0ec63d392904eee34ef1cbde2d8798f79a3663950e42fbbc25fd1bd6f7/types-objgraph-3.6.0.20240907.tar.gz", hash = "sha256:2e3dee675843ae387889731550b0ddfed06e9420946cf78a4bca565b5fc53634", size = 2928, upload-time = "2024-09-07T02:35:21.214Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/c9/6d647a947f3937b19bcc6d52262921ddad60d90060ff66511a4bd7e990c5/types_objgraph-3.6.0.20240907-py3-none-any.whl", hash = "sha256:67207633a9b5789ee1911d740b269c3371081b79c0d8f68b00e7b8539f5c43f5", size = 3314 }, + { url = "https://files.pythonhosted.org/packages/16/c9/6d647a947f3937b19bcc6d52262921ddad60d90060ff66511a4bd7e990c5/types_objgraph-3.6.0.20240907-py3-none-any.whl", hash = "sha256:67207633a9b5789ee1911d740b269c3371081b79c0d8f68b00e7b8539f5c43f5", size = 3314, upload-time = "2024-09-07T02:35:19.865Z" }, ] [[package]] name = "types-olefile" version = "0.47.0.20240806" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/18/9d87a1bc394323ce22690308c751680c4301fc3fbe47cd58e16d760b563a/types-olefile-0.47.0.20240806.tar.gz", hash = "sha256:96490f208cbb449a52283855319d73688ba9167ae58858ef8c506bf7ca2c6b67", size = 4369 } +sdist = { url = "https://files.pythonhosted.org/packages/49/18/9d87a1bc394323ce22690308c751680c4301fc3fbe47cd58e16d760b563a/types-olefile-0.47.0.20240806.tar.gz", hash = "sha256:96490f208cbb449a52283855319d73688ba9167ae58858ef8c506bf7ca2c6b67", size = 4369, upload-time = "2024-08-06T02:30:01.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/4d/f8acae53dd95353f8a789a06ea27423ae41f2067eb6ce92946fdc6a1f7a7/types_olefile-0.47.0.20240806-py3-none-any.whl", hash = "sha256:c760a3deab7adb87a80d33b0e4edbbfbab865204a18d5121746022d7f8555118", size = 4758 }, + { url = "https://files.pythonhosted.org/packages/a9/4d/f8acae53dd95353f8a789a06ea27423ae41f2067eb6ce92946fdc6a1f7a7/types_olefile-0.47.0.20240806-py3-none-any.whl", hash = "sha256:c760a3deab7adb87a80d33b0e4edbbfbab865204a18d5121746022d7f8555118", size = 4758, upload-time = "2024-08-06T02:30:01.15Z" }, ] [[package]] name = "types-openpyxl" version = "3.1.5.20250602" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/d4/33cc2f331cde82206aa4ec7d8db408beca65964785f438c6d2505d828178/types_openpyxl-3.1.5.20250602.tar.gz", hash = "sha256:d19831482022fc933780d6e9d6990464c18c2ec5f14786fea862f72c876980b5", size = 100608 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/d4/33cc2f331cde82206aa4ec7d8db408beca65964785f438c6d2505d828178/types_openpyxl-3.1.5.20250602.tar.gz", hash = "sha256:d19831482022fc933780d6e9d6990464c18c2ec5f14786fea862f72c876980b5", size = 100608, upload-time = "2025-06-02T03:14:40.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/69/5b924a20a4d441ec2160e94085b9fa9358dc27edde10080d71209c59101d/types_openpyxl-3.1.5.20250602-py3-none-any.whl", hash = "sha256:1f82211e086902318f6a14b5d8d865102362fda7cb82f3d63ac4dff47a1f164b", size = 165922 }, + { url = "https://files.pythonhosted.org/packages/2e/69/5b924a20a4d441ec2160e94085b9fa9358dc27edde10080d71209c59101d/types_openpyxl-3.1.5.20250602-py3-none-any.whl", hash = "sha256:1f82211e086902318f6a14b5d8d865102362fda7cb82f3d63ac4dff47a1f164b", size = 165922, upload-time = "2025-06-02T03:14:39.226Z" }, ] [[package]] name = "types-pexpect" version = "4.9.0.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/a3/3943fcb94c12af29a88c346b588f1eda180b8b99aeb388a046b25072732c/types_pexpect-4.9.0.20250516.tar.gz", hash = "sha256:7baed9ee566fa24034a567cbec56a5cff189a021344e84383b14937b35d83881", size = 13285 } +sdist = { url = "https://files.pythonhosted.org/packages/92/a3/3943fcb94c12af29a88c346b588f1eda180b8b99aeb388a046b25072732c/types_pexpect-4.9.0.20250516.tar.gz", hash = "sha256:7baed9ee566fa24034a567cbec56a5cff189a021344e84383b14937b35d83881", size = 13285, upload-time = "2025-05-16T03:08:33.327Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/d4/3128ae3365b46b9c4a33202af79b0e0d9d4308a6348a3317ce2331fea6cb/types_pexpect-4.9.0.20250516-py3-none-any.whl", hash = "sha256:84cbd7ae9da577c0d2629d4e4fd53cf074cd012296e01fd4fa1031e01973c28a", size = 17081 }, + { url = "https://files.pythonhosted.org/packages/e1/d4/3128ae3365b46b9c4a33202af79b0e0d9d4308a6348a3317ce2331fea6cb/types_pexpect-4.9.0.20250516-py3-none-any.whl", hash = "sha256:84cbd7ae9da577c0d2629d4e4fd53cf074cd012296e01fd4fa1031e01973c28a", size = 17081, upload-time = "2025-05-16T03:08:32.127Z" }, ] [[package]] name = "types-protobuf" version = "5.29.1.20250403" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/6d/62a2e73b966c77609560800004dd49a926920dd4976a9fdd86cf998e7048/types_protobuf-5.29.1.20250403.tar.gz", hash = "sha256:7ff44f15022119c9d7558ce16e78b2d485bf7040b4fadced4dd069bb5faf77a2", size = 59413 } +sdist = { url = "https://files.pythonhosted.org/packages/78/6d/62a2e73b966c77609560800004dd49a926920dd4976a9fdd86cf998e7048/types_protobuf-5.29.1.20250403.tar.gz", hash = "sha256:7ff44f15022119c9d7558ce16e78b2d485bf7040b4fadced4dd069bb5faf77a2", size = 59413, upload-time = "2025-04-02T10:07:17.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e3/b74dcc2797b21b39d5a4f08a8b08e20369b4ca250d718df7af41a60dd9f0/types_protobuf-5.29.1.20250403-py3-none-any.whl", hash = "sha256:c71de04106a2d54e5b2173d0a422058fae0ef2d058d70cf369fb797bf61ffa59", size = 73874 }, + { url = "https://files.pythonhosted.org/packages/69/e3/b74dcc2797b21b39d5a4f08a8b08e20369b4ca250d718df7af41a60dd9f0/types_protobuf-5.29.1.20250403-py3-none-any.whl", hash = "sha256:c71de04106a2d54e5b2173d0a422058fae0ef2d058d70cf369fb797bf61ffa59", size = 73874, upload-time = "2025-04-02T10:07:15.755Z" }, ] [[package]] name = "types-psutil" version = "7.0.0.20250601" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/af/767b92be7de4105f5e2e87a53aac817164527c4a802119ad5b4e23028f7c/types_psutil-7.0.0.20250601.tar.gz", hash = "sha256:71fe9c4477a7e3d4f1233862f0877af87bff057ff398f04f4e5c0ca60aded197", size = 20297 } +sdist = { url = "https://files.pythonhosted.org/packages/c8/af/767b92be7de4105f5e2e87a53aac817164527c4a802119ad5b4e23028f7c/types_psutil-7.0.0.20250601.tar.gz", hash = "sha256:71fe9c4477a7e3d4f1233862f0877af87bff057ff398f04f4e5c0ca60aded197", size = 20297, upload-time = "2025-06-01T03:25:16.698Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/85/864c663a924a34e0d87bd10ead4134bb4ab6269fa02daaa5dd644ac478c5/types_psutil-7.0.0.20250601-py3-none-any.whl", hash = "sha256:0c372e2d1b6529938a080a6ba4a9358e3dfc8526d82fabf40c1ef9325e4ca52e", size = 23106 }, + { url = "https://files.pythonhosted.org/packages/8d/85/864c663a924a34e0d87bd10ead4134bb4ab6269fa02daaa5dd644ac478c5/types_psutil-7.0.0.20250601-py3-none-any.whl", hash = "sha256:0c372e2d1b6529938a080a6ba4a9358e3dfc8526d82fabf40c1ef9325e4ca52e", size = 23106, upload-time = "2025-06-01T03:25:15.386Z" }, ] [[package]] name = "types-psycopg2" version = "2.9.21.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/55/3f94eff9d1a1402f39e19523a90117fe6c97d7fc61957e7ee3e3052c75e1/types_psycopg2-2.9.21.20250516.tar.gz", hash = "sha256:6721018279175cce10b9582202e2a2b4a0da667857ccf82a97691bdb5ecd610f", size = 26514 } +sdist = { url = "https://files.pythonhosted.org/packages/68/55/3f94eff9d1a1402f39e19523a90117fe6c97d7fc61957e7ee3e3052c75e1/types_psycopg2-2.9.21.20250516.tar.gz", hash = "sha256:6721018279175cce10b9582202e2a2b4a0da667857ccf82a97691bdb5ecd610f", size = 26514, upload-time = "2025-05-16T03:07:45.786Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/50/f5d74945ab09b9a3e966ad39027ac55998f917eca72ede7929eab962b5db/types_psycopg2-2.9.21.20250516-py3-none-any.whl", hash = "sha256:2a9212d1e5e507017b31486ce8147634d06b85d652769d7a2d91d53cb4edbd41", size = 24846 }, + { url = "https://files.pythonhosted.org/packages/39/50/f5d74945ab09b9a3e966ad39027ac55998f917eca72ede7929eab962b5db/types_psycopg2-2.9.21.20250516-py3-none-any.whl", hash = "sha256:2a9212d1e5e507017b31486ce8147634d06b85d652769d7a2d91d53cb4edbd41", size = 24846, upload-time = "2025-05-16T03:07:44.849Z" }, ] [[package]] @@ -5808,18 +5880,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-docutils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/9a/c1ea3f59001e9d13b93ec8acf02c75b47832423f17471295b8ceebc48a65/types_pygments-2.19.0.20250516.tar.gz", hash = "sha256:b53fd07e197f0e7be38ee19598bd99c78be5ca5f9940849c843be74a2f81ab58", size = 18485 } +sdist = { url = "https://files.pythonhosted.org/packages/71/9a/c1ea3f59001e9d13b93ec8acf02c75b47832423f17471295b8ceebc48a65/types_pygments-2.19.0.20250516.tar.gz", hash = "sha256:b53fd07e197f0e7be38ee19598bd99c78be5ca5f9940849c843be74a2f81ab58", size = 18485, upload-time = "2025-05-16T03:09:30.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/0b/32ce3ad35983bf4f603c43cfb00559b37bb5ed90ac4ef9f1d5564b8e4034/types_pygments-2.19.0.20250516-py3-none-any.whl", hash = "sha256:db27de8b59591389cd7d14792483892c021c73b8389ef55fef40a48aa371fbcc", size = 25440 }, + { url = "https://files.pythonhosted.org/packages/a7/0b/32ce3ad35983bf4f603c43cfb00559b37bb5ed90ac4ef9f1d5564b8e4034/types_pygments-2.19.0.20250516-py3-none-any.whl", hash = "sha256:db27de8b59591389cd7d14792483892c021c73b8389ef55fef40a48aa371fbcc", size = 25440, upload-time = "2025-05-16T03:09:29.185Z" }, ] [[package]] name = "types-pymysql" -version = "1.1.0.20250516" +version = "1.1.0.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/11/cdaa90b82cb25c5e04e75f0b0616872aa5775b001096779375084f8dbbcf/types_pymysql-1.1.0.20250516.tar.gz", hash = "sha256:fea4a9776101cf893dfc868f42ce10d2e46dcc498c792cc7c9c0fe00cb744234", size = 19640 } +sdist = { url = "https://files.pythonhosted.org/packages/65/a3/db349a06c64b8c041c165fc470b81d37404ec342014625c7a6b7f7a4f680/types_pymysql-1.1.0.20250708.tar.gz", hash = "sha256:2cbd7cfcf9313eda784910578c4f1d06f8cc03a15cd30ce588aa92dd6255011d", size = 21715, upload-time = "2025-07-08T03:13:56.463Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/64/129656e04ddda35d69faae914ce67cf60d83407ddd7afdef1e7c50bbb74a/types_pymysql-1.1.0.20250516-py3-none-any.whl", hash = "sha256:41c87a832e3ff503d5120cc6cebd64f6dcb3c407d9580a98b2cb3e3bcd109aa6", size = 20328 }, + { url = "https://files.pythonhosted.org/packages/88/e5/7f72c520f527175b6455e955426fd4f971128b4fa2f8ab2f505f254a1ddc/types_pymysql-1.1.0.20250708-py3-none-any.whl", hash = "sha256:9252966d2795945b2a7a53d5cdc49fe8e4e2f3dde4c104ed7fc782a83114e365", size = 22860, upload-time = "2025-07-08T03:13:55.367Z" }, ] [[package]] @@ -5830,66 +5902,75 @@ dependencies = [ { name = "cryptography" }, { name = "types-cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, ] [[package]] name = "types-python-dateutil" -version = "2.9.0.20250516" +version = "2.9.0.20250708" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/95/6bdde7607da2e1e99ec1c1672a759d42f26644bbacf939916e086db34870/types_python_dateutil-2.9.0.20250708.tar.gz", hash = "sha256:ccdbd75dab2d6c9696c350579f34cffe2c281e4c5f27a585b2a2438dd1d5c8ab", size = 15834, upload-time = "2025-07-08T03:14:03.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/52/43e70a8e57fefb172c22a21000b03ebcc15e47e97f5cb8495b9c2832efb4/types_python_dateutil-2.9.0.20250708-py3-none-any.whl", hash = "sha256:4d6d0cc1cc4d24a2dc3816024e502564094497b713f7befda4d5bc7a8e3fd21f", size = 17724, upload-time = "2025-07-08T03:14:02.593Z" }, +] + +[[package]] +name = "types-python-http-client" +version = "3.3.7.20250708" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943 } +sdist = { url = "https://files.pythonhosted.org/packages/55/a0/0ad93698a3ebc6846ca23aca20ff6f6f8ebe7b4f0c1de7f19e87c03dbe8f/types_python_http_client-3.3.7.20250708.tar.gz", hash = "sha256:5f85b32dc64671a4e5e016142169aa187c5abed0b196680944e4efd3d5ce3322", size = 7707, upload-time = "2025-07-08T03:14:36.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356 }, + { url = "https://files.pythonhosted.org/packages/85/4f/b88274658cf489e35175be8571c970e9a1219713bafd8fc9e166d7351ecb/types_python_http_client-3.3.7.20250708-py3-none-any.whl", hash = "sha256:e2fc253859decab36713d82fc7f205868c3ddeaee79dbb55956ad9ca77abe12b", size = 8890, upload-time = "2025-07-08T03:14:35.506Z" }, ] [[package]] name = "types-pytz" version = "2025.2.0.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/72/b0e711fd90409f5a76c75349055d3eb19992c110f0d2d6aabbd6cfbc14bf/types_pytz-2025.2.0.20250516.tar.gz", hash = "sha256:e1216306f8c0d5da6dafd6492e72eb080c9a166171fa80dd7a1990fd8be7a7b3", size = 10940 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/72/b0e711fd90409f5a76c75349055d3eb19992c110f0d2d6aabbd6cfbc14bf/types_pytz-2025.2.0.20250516.tar.gz", hash = "sha256:e1216306f8c0d5da6dafd6492e72eb080c9a166171fa80dd7a1990fd8be7a7b3", size = 10940, upload-time = "2025-05-16T03:07:01.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ba/e205cd11c1c7183b23c97e4bcd1de7bc0633e2e867601c32ecfc6ad42675/types_pytz-2025.2.0.20250516-py3-none-any.whl", hash = "sha256:e0e0c8a57e2791c19f718ed99ab2ba623856b11620cb6b637e5f62ce285a7451", size = 10136 }, + { url = "https://files.pythonhosted.org/packages/c1/ba/e205cd11c1c7183b23c97e4bcd1de7bc0633e2e867601c32ecfc6ad42675/types_pytz-2025.2.0.20250516-py3-none-any.whl", hash = "sha256:e0e0c8a57e2791c19f718ed99ab2ba623856b11620cb6b637e5f62ce285a7451", size = 10136, upload-time = "2025-05-16T03:07:01.075Z" }, ] [[package]] name = "types-pywin32" version = "310.0.0.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/bc/c7be2934a37cc8c645c945ca88450b541e482c4df3ac51e5556377d34811/types_pywin32-310.0.0.20250516.tar.gz", hash = "sha256:91e5bfc033f65c9efb443722eff8101e31d690dd9a540fa77525590d3da9cc9d", size = 328459 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/bc/c7be2934a37cc8c645c945ca88450b541e482c4df3ac51e5556377d34811/types_pywin32-310.0.0.20250516.tar.gz", hash = "sha256:91e5bfc033f65c9efb443722eff8101e31d690dd9a540fa77525590d3da9cc9d", size = 328459, upload-time = "2025-05-16T03:07:57.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/72/469e4cc32399dbe6c843e38fdb6d04fee755e984e137c0da502f74d3ac59/types_pywin32-310.0.0.20250516-py3-none-any.whl", hash = "sha256:f9ef83a1ec3e5aae2b0e24c5f55ab41272b5dfeaabb9a0451d33684c9545e41a", size = 390411 }, + { url = "https://files.pythonhosted.org/packages/9b/72/469e4cc32399dbe6c843e38fdb6d04fee755e984e137c0da502f74d3ac59/types_pywin32-310.0.0.20250516-py3-none-any.whl", hash = "sha256:f9ef83a1ec3e5aae2b0e24c5f55ab41272b5dfeaabb9a0451d33684c9545e41a", size = 390411, upload-time = "2025-05-16T03:07:56.282Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312 }, + { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" }, ] [[package]] name = "types-regex" version = "2024.11.6.20250403" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/75/012b90c8557d3abb3b58a9073a94d211c8f75c9b2e26bf0d8af7ecf7bc78/types_regex-2024.11.6.20250403.tar.gz", hash = "sha256:3fdf2a70bbf830de4b3a28e9649a52d43dabb57cdb18fbfe2252eefb53666665", size = 12394 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/75/012b90c8557d3abb3b58a9073a94d211c8f75c9b2e26bf0d8af7ecf7bc78/types_regex-2024.11.6.20250403.tar.gz", hash = "sha256:3fdf2a70bbf830de4b3a28e9649a52d43dabb57cdb18fbfe2252eefb53666665", size = 12394, upload-time = "2025-04-03T02:54:35.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/49/67200c4708f557be6aa4ecdb1fa212d67a10558c5240251efdc799cca22f/types_regex-2024.11.6.20250403-py3-none-any.whl", hash = "sha256:e22c0f67d73f4b4af6086a340f387b6f7d03bed8a0bb306224b75c51a29b0001", size = 10396 }, + { url = "https://files.pythonhosted.org/packages/61/49/67200c4708f557be6aa4ecdb1fa212d67a10558c5240251efdc799cca22f/types_regex-2024.11.6.20250403-py3-none-any.whl", hash = "sha256:e22c0f67d73f4b4af6086a340f387b6f7d03bed8a0bb306224b75c51a29b0001", size = 10396, upload-time = "2025-04-03T02:54:34.555Z" }, ] [[package]] name = "types-requests" -version = "2.32.0.20250602" +version = "2.32.4.20250611" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/b0/5321e6eeba5d59e4347fcf9bf06a5052f085c3aa0f4876230566d6a4dc97/types_requests-2.32.0.20250602.tar.gz", hash = "sha256:ee603aeefec42051195ae62ca7667cd909a2f8128fdf8aad9e8a5219ecfab3bf", size = 23042 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/18/9b782980e575c6581d5c0c1c99f4c6f89a1d7173dad072ee96b2756c02e6/types_requests-2.32.0.20250602-py3-none-any.whl", hash = "sha256:f4f335f87779b47ce10b8b8597b409130299f6971ead27fead4fe7ba6ea3e726", size = 20638 }, + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, ] [[package]] @@ -5900,27 +5981,27 @@ dependencies = [ { name = "types-oauthlib" }, { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/7b/1803a83dbccf0698a9fb70a444d12f1dcb0f49a5d8a6327a1e53fac19e15/types_requests_oauthlib-2.0.0.20250516.tar.gz", hash = "sha256:2a384b6ca080bd1eb30a88e14836237dc43d217892fddf869f03aea65213e0d4", size = 11034 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/7b/1803a83dbccf0698a9fb70a444d12f1dcb0f49a5d8a6327a1e53fac19e15/types_requests_oauthlib-2.0.0.20250516.tar.gz", hash = "sha256:2a384b6ca080bd1eb30a88e14836237dc43d217892fddf869f03aea65213e0d4", size = 11034, upload-time = "2025-05-16T03:09:45.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/3c/1bc76f1097cc4978cc97df11524f47559f8927fb2a2807375947bd185189/types_requests_oauthlib-2.0.0.20250516-py3-none-any.whl", hash = "sha256:faf417c259a3ae54c1b72c77032c07af3025ed90164c905fb785d21e8580139c", size = 14343 }, + { url = "https://files.pythonhosted.org/packages/e8/3c/1bc76f1097cc4978cc97df11524f47559f8927fb2a2807375947bd185189/types_requests_oauthlib-2.0.0.20250516-py3-none-any.whl", hash = "sha256:faf417c259a3ae54c1b72c77032c07af3025ed90164c905fb785d21e8580139c", size = 14343, upload-time = "2025-05-16T03:09:43.874Z" }, ] [[package]] name = "types-s3transfer" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175 } +sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175, upload-time = "2025-05-28T02:16:07.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588 }, + { url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload-time = "2025-05-28T02:16:06.709Z" }, ] [[package]] name = "types-setuptools" version = "80.9.0.20250529" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/66/1b276526aad4696a9519919e637801f2c103419d2c248a6feb2729e034d1/types_setuptools-80.9.0.20250529.tar.gz", hash = "sha256:79e088ba0cba2186c8d6499cbd3e143abb142d28a44b042c28d3148b1e353c91", size = 41337 } +sdist = { url = "https://files.pythonhosted.org/packages/79/66/1b276526aad4696a9519919e637801f2c103419d2c248a6feb2729e034d1/types_setuptools-80.9.0.20250529.tar.gz", hash = "sha256:79e088ba0cba2186c8d6499cbd3e143abb142d28a44b042c28d3148b1e353c91", size = 41337, upload-time = "2025-05-29T03:07:34.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d8/83790d67ec771bf029a45ff1bd1aedbb738d8aa58c09dd0cc3033eea0e69/types_setuptools-80.9.0.20250529-py3-none-any.whl", hash = "sha256:00dfcedd73e333a430e10db096e4d46af93faf9314f832f13b6bbe3d6757e95f", size = 63263 }, + { url = "https://files.pythonhosted.org/packages/1b/d8/83790d67ec771bf029a45ff1bd1aedbb738d8aa58c09dd0cc3033eea0e69/types_setuptools-80.9.0.20250529-py3-none-any.whl", hash = "sha256:00dfcedd73e333a430e10db096e4d46af93faf9314f832f13b6bbe3d6757e95f", size = 63263, upload-time = "2025-05-29T03:07:33.064Z" }, ] [[package]] @@ -5930,27 +6011,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/55/c71a25fd3fc9200df4d0b5fd2f6d74712a82f9a8bbdd90cefb9e6aee39dd/types_shapely-2.0.0.20250404.tar.gz", hash = "sha256:863f540b47fa626c33ae64eae06df171f9ab0347025d4458d2df496537296b4f", size = 25066 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/55/c71a25fd3fc9200df4d0b5fd2f6d74712a82f9a8bbdd90cefb9e6aee39dd/types_shapely-2.0.0.20250404.tar.gz", hash = "sha256:863f540b47fa626c33ae64eae06df171f9ab0347025d4458d2df496537296b4f", size = 25066, upload-time = "2025-04-04T02:54:30.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/ff/7f4d414eb81534ba2476f3d54f06f1463c2ebf5d663fd10cff16ba607dd6/types_shapely-2.0.0.20250404-py3-none-any.whl", hash = "sha256:170fb92f5c168a120db39b3287697fdec5c93ef3e1ad15e52552c36b25318821", size = 36350 }, + { url = "https://files.pythonhosted.org/packages/ce/ff/7f4d414eb81534ba2476f3d54f06f1463c2ebf5d663fd10cff16ba607dd6/types_shapely-2.0.0.20250404-py3-none-any.whl", hash = "sha256:170fb92f5c168a120db39b3287697fdec5c93ef3e1ad15e52552c36b25318821", size = 36350, upload-time = "2025-04-04T02:54:29.506Z" }, ] [[package]] name = "types-simplejson" version = "3.20.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489 } +sdist = { url = "https://files.pythonhosted.org/packages/af/14/e26fc55e1ea56f9ea470917d3e2f8240e6d043ca914181021d04115ae0f7/types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2", size = 10489, upload-time = "2025-03-26T02:53:35.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462 }, + { url = "https://files.pythonhosted.org/packages/76/bf/d3f3a5ba47fd18115e8446d39f025b85905d2008677c29ee4d03b4cddd57/types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7", size = 10462, upload-time = "2025-03-26T02:53:35.036Z" }, ] [[package]] name = "types-six" version = "1.17.0.20250515" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/78/344047eeced8d230140aa3d9503aa969acb61c6095e7308bbc1ff1de3865/types_six-1.17.0.20250515.tar.gz", hash = "sha256:f4f7f0398cb79304e88397336e642b15e96fbeacf5b96d7625da366b069d2d18", size = 15598 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/78/344047eeced8d230140aa3d9503aa969acb61c6095e7308bbc1ff1de3865/types_six-1.17.0.20250515.tar.gz", hash = "sha256:f4f7f0398cb79304e88397336e642b15e96fbeacf5b96d7625da366b069d2d18", size = 15598, upload-time = "2025-05-15T03:04:19.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/85/5ee1c8e35b33b9c8ea1816d5a4e119c27f8bb1539b73b1f636f07aa64750/types_six-1.17.0.20250515-py3-none-any.whl", hash = "sha256:adfaa9568caf35e03d80ffa4ed765c33b282579c869b40bf4b6009c7d8db3fb1", size = 19987 }, + { url = "https://files.pythonhosted.org/packages/d1/85/5ee1c8e35b33b9c8ea1816d5a4e119c27f8bb1539b73b1f636f07aa64750/types_six-1.17.0.20250515-py3-none-any.whl", hash = "sha256:adfaa9568caf35e03d80ffa4ed765c33b282579c869b40bf4b6009c7d8db3fb1", size = 19987, upload-time = "2025-05-15T03:04:18.556Z" }, ] [[package]] @@ -5962,9 +6043,9 @@ dependencies = [ { name = "types-protobuf" }, { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/18/b726d886e7af565c4439d2c8d32e510651be40807e2a66aaea2ed75d7c82/types_tensorflow-2.18.0.20250516.tar.gz", hash = "sha256:5777e1848e52b1f4a87b44ce1ec738b7407a744669bab87ec0f5f1e0ce6bd1fe", size = 257705 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/18/b726d886e7af565c4439d2c8d32e510651be40807e2a66aaea2ed75d7c82/types_tensorflow-2.18.0.20250516.tar.gz", hash = "sha256:5777e1848e52b1f4a87b44ce1ec738b7407a744669bab87ec0f5f1e0ce6bd1fe", size = 257705, upload-time = "2025-05-16T03:09:41.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/fd/0d8fbc7172fa7cca345c61a949952df8906f6da161dfbb4305c670aeabad/types_tensorflow-2.18.0.20250516-py3-none-any.whl", hash = "sha256:e8681f8c2a60f87f562df1472790c1e930895e7e463c4c65d1be98d8d908e45e", size = 329211 }, + { url = "https://files.pythonhosted.org/packages/96/fd/0d8fbc7172fa7cca345c61a949952df8906f6da161dfbb4305c670aeabad/types_tensorflow-2.18.0.20250516-py3-none-any.whl", hash = "sha256:e8681f8c2a60f87f562df1472790c1e930895e7e463c4c65d1be98d8d908e45e", size = 329211, upload-time = "2025-05-16T03:09:40.111Z" }, ] [[package]] @@ -5974,27 +6055,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/07/eb40de2dc2ff2d1a53180330981b1bdb42313ab4e1b11195d8d64c878b3c/types_tqdm-4.67.0.20250516.tar.gz", hash = "sha256:230ccab8a332d34f193fc007eb132a6ef54b4512452e718bf21ae0a7caeb5a6b", size = 17232 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/07/eb40de2dc2ff2d1a53180330981b1bdb42313ab4e1b11195d8d64c878b3c/types_tqdm-4.67.0.20250516.tar.gz", hash = "sha256:230ccab8a332d34f193fc007eb132a6ef54b4512452e718bf21ae0a7caeb5a6b", size = 17232, upload-time = "2025-05-16T03:09:52.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/92/df621429f098fc573a63a8ba348e731c3051b397df0cff278f8887f28d24/types_tqdm-4.67.0.20250516-py3-none-any.whl", hash = "sha256:1dd9b2c65273f2342f37e5179bc6982df86b6669b3376efc12aef0a29e35d36d", size = 24032 }, + { url = "https://files.pythonhosted.org/packages/3b/92/df621429f098fc573a63a8ba348e731c3051b397df0cff278f8887f28d24/types_tqdm-4.67.0.20250516-py3-none-any.whl", hash = "sha256:1dd9b2c65273f2342f37e5179bc6982df86b6669b3376efc12aef0a29e35d36d", size = 24032, upload-time = "2025-05-16T03:09:51.226Z" }, ] [[package]] name = "types-ujson" version = "5.10.0.20250326" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/5c/c974451c4babdb4ae3588925487edde492d59a8403010b4642a554d09954/types_ujson-5.10.0.20250326.tar.gz", hash = "sha256:5469e05f2c31ecb3c4c0267cc8fe41bcd116826fbb4ded69801a645c687dd014", size = 8340, upload-time = "2025-03-26T02:53:39.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644 }, + { url = "https://files.pythonhosted.org/packages/3e/c9/8a73a5f8fa6e70fc02eed506d5ac0ae9ceafbd2b8c9ad34a7de0f29900d6/types_ujson-5.10.0.20250326-py3-none-any.whl", hash = "sha256:acc0913f569def62ef6a892c8a47703f65d05669a3252391a97765cf207dca5b", size = 7644, upload-time = "2025-03-26T02:53:38.2Z" }, ] [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] @@ -6005,9 +6086,9 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, ] [[package]] @@ -6017,18 +6098,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] [[package]] @@ -6038,37 +6119,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 }, + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, ] [[package]] name = "ujson" version = "5.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/54/6f2bdac7117e89a47de4511c9f01732a283457ab1bf856e1e51aa861619e/ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532", size = 7154214 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/ca/ae3a6ca5b4f82ce654d6ac3dde5e59520537e20939592061ba506f4e569a/ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b", size = 57753 }, - { url = "https://files.pythonhosted.org/packages/34/5f/c27fa9a1562c96d978c39852b48063c3ca480758f3088dcfc0f3b09f8e93/ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0", size = 54092 }, - { url = "https://files.pythonhosted.org/packages/19/f3/1431713de9e5992e5e33ba459b4de28f83904233958855d27da820a101f9/ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae", size = 51675 }, - { url = "https://files.pythonhosted.org/packages/d3/93/de6fff3ae06351f3b1c372f675fe69bc180f93d237c9e496c05802173dd6/ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d", size = 53246 }, - { url = "https://files.pythonhosted.org/packages/26/73/db509fe1d7da62a15c0769c398cec66bdfc61a8bdffaf7dfa9d973e3d65c/ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e", size = 58182 }, - { url = "https://files.pythonhosted.org/packages/fc/a8/6be607fa3e1fa3e1c9b53f5de5acad33b073b6cc9145803e00bcafa729a8/ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908", size = 584493 }, - { url = "https://files.pythonhosted.org/packages/c8/c7/33822c2f1a8175e841e2bc378ffb2c1109ce9280f14cedb1b2fa0caf3145/ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b", size = 656038 }, - { url = "https://files.pythonhosted.org/packages/51/b8/5309fbb299d5fcac12bbf3db20896db5178392904abe6b992da233dc69d6/ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d", size = 597643 }, - { url = "https://files.pythonhosted.org/packages/5f/64/7b63043b95dd78feed401b9973958af62645a6d19b72b6e83d1ea5af07e0/ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120", size = 38342 }, - { url = "https://files.pythonhosted.org/packages/7a/13/a3cd1fc3a1126d30b558b6235c05e2d26eeaacba4979ee2fd2b5745c136d/ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99", size = 41923 }, - { url = "https://files.pythonhosted.org/packages/16/7e/c37fca6cd924931fa62d615cdbf5921f34481085705271696eff38b38867/ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c", size = 57834 }, - { url = "https://files.pythonhosted.org/packages/fb/44/2753e902ee19bf6ccaf0bda02f1f0037f92a9769a5d31319905e3de645b4/ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f", size = 54119 }, - { url = "https://files.pythonhosted.org/packages/d2/06/2317433e394450bc44afe32b6c39d5a51014da4c6f6cfc2ae7bf7b4a2922/ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399", size = 51658 }, - { url = "https://files.pythonhosted.org/packages/5b/3a/2acf0da085d96953580b46941504aa3c91a1dd38701b9e9bfa43e2803467/ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e", size = 53370 }, - { url = "https://files.pythonhosted.org/packages/03/32/737e6c4b1841720f88ae88ec91f582dc21174bd40742739e1fa16a0c9ffa/ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320", size = 58278 }, - { url = "https://files.pythonhosted.org/packages/8a/dc/3fda97f1ad070ccf2af597fb67dde358bc698ffecebe3bc77991d60e4fe5/ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164", size = 584418 }, - { url = "https://files.pythonhosted.org/packages/d7/57/e4083d774fcd8ff3089c0ff19c424abe33f23e72c6578a8172bf65131992/ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01", size = 656126 }, - { url = "https://files.pythonhosted.org/packages/0d/c3/8c6d5f6506ca9fcedd5a211e30a7d5ee053dc05caf23dae650e1f897effb/ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c", size = 597795 }, - { url = "https://files.pythonhosted.org/packages/34/5a/a231f0cd305a34cf2d16930304132db3a7a8c3997b367dd38fc8f8dfae36/ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437", size = 38495 }, - { url = "https://files.pythonhosted.org/packages/30/b7/18b841b44760ed298acdb150608dccdc045c41655e0bae4441f29bcab872/ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c", size = 42088 }, +sdist = { url = "https://files.pythonhosted.org/packages/6e/54/6f2bdac7117e89a47de4511c9f01732a283457ab1bf856e1e51aa861619e/ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532", size = 7154214, upload-time = "2023-12-10T22:50:34.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/ca/ae3a6ca5b4f82ce654d6ac3dde5e59520537e20939592061ba506f4e569a/ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b", size = 57753, upload-time = "2023-12-10T22:49:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/34/5f/c27fa9a1562c96d978c39852b48063c3ca480758f3088dcfc0f3b09f8e93/ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0", size = 54092, upload-time = "2023-12-10T22:49:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/19/f3/1431713de9e5992e5e33ba459b4de28f83904233958855d27da820a101f9/ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae", size = 51675, upload-time = "2023-12-10T22:49:06.449Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/de6fff3ae06351f3b1c372f675fe69bc180f93d237c9e496c05802173dd6/ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d", size = 53246, upload-time = "2023-12-10T22:49:07.691Z" }, + { url = "https://files.pythonhosted.org/packages/26/73/db509fe1d7da62a15c0769c398cec66bdfc61a8bdffaf7dfa9d973e3d65c/ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e", size = 58182, upload-time = "2023-12-10T22:49:08.89Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a8/6be607fa3e1fa3e1c9b53f5de5acad33b073b6cc9145803e00bcafa729a8/ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908", size = 584493, upload-time = "2023-12-10T22:49:11.043Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c7/33822c2f1a8175e841e2bc378ffb2c1109ce9280f14cedb1b2fa0caf3145/ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b", size = 656038, upload-time = "2023-12-10T22:49:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/51/b8/5309fbb299d5fcac12bbf3db20896db5178392904abe6b992da233dc69d6/ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d", size = 597643, upload-time = "2023-12-10T22:49:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/5f/64/7b63043b95dd78feed401b9973958af62645a6d19b72b6e83d1ea5af07e0/ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120", size = 38342, upload-time = "2023-12-10T22:49:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/7a/13/a3cd1fc3a1126d30b558b6235c05e2d26eeaacba4979ee2fd2b5745c136d/ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99", size = 41923, upload-time = "2023-12-10T22:49:17.983Z" }, + { url = "https://files.pythonhosted.org/packages/16/7e/c37fca6cd924931fa62d615cdbf5921f34481085705271696eff38b38867/ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c", size = 57834, upload-time = "2023-12-10T22:49:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/fb/44/2753e902ee19bf6ccaf0bda02f1f0037f92a9769a5d31319905e3de645b4/ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f", size = 54119, upload-time = "2023-12-10T22:49:21.039Z" }, + { url = "https://files.pythonhosted.org/packages/d2/06/2317433e394450bc44afe32b6c39d5a51014da4c6f6cfc2ae7bf7b4a2922/ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399", size = 51658, upload-time = "2023-12-10T22:49:22.494Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3a/2acf0da085d96953580b46941504aa3c91a1dd38701b9e9bfa43e2803467/ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e", size = 53370, upload-time = "2023-12-10T22:49:24.045Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/737e6c4b1841720f88ae88ec91f582dc21174bd40742739e1fa16a0c9ffa/ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320", size = 58278, upload-time = "2023-12-10T22:49:25.261Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/3fda97f1ad070ccf2af597fb67dde358bc698ffecebe3bc77991d60e4fe5/ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164", size = 584418, upload-time = "2023-12-10T22:49:27.573Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/e4083d774fcd8ff3089c0ff19c424abe33f23e72c6578a8172bf65131992/ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01", size = 656126, upload-time = "2023-12-10T22:49:29.509Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/8c6d5f6506ca9fcedd5a211e30a7d5ee053dc05caf23dae650e1f897effb/ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c", size = 597795, upload-time = "2023-12-10T22:49:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/34/5a/a231f0cd305a34cf2d16930304132db3a7a8c3997b367dd38fc8f8dfae36/ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437", size = 38495, upload-time = "2023-12-10T22:49:33.2Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/18b841b44760ed298acdb150608dccdc045c41655e0bae4441f29bcab872/ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c", size = 42088, upload-time = "2023-12-10T22:49:34.921Z" }, ] [[package]] @@ -6098,9 +6179,9 @@ dependencies = [ { name = "unstructured-client" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/31/98c4c78e305d1294888adf87fd5ee30577a4c393951341ca32b43f167f1e/unstructured-0.16.25.tar.gz", hash = "sha256:73b9b0f51dbb687af572ecdb849a6811710b9cac797ddeab8ee80fa07d8aa5e6", size = 1683097 } +sdist = { url = "https://files.pythonhosted.org/packages/64/31/98c4c78e305d1294888adf87fd5ee30577a4c393951341ca32b43f167f1e/unstructured-0.16.25.tar.gz", hash = "sha256:73b9b0f51dbb687af572ecdb849a6811710b9cac797ddeab8ee80fa07d8aa5e6", size = 1683097, upload-time = "2025-03-07T11:19:39.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4f/ad08585b5c8a33c82ea119494c4d3023f4796958c56e668b15cc282ec0a0/unstructured-0.16.25-py3-none-any.whl", hash = "sha256:14719ccef2830216cf1c5bf654f75e2bf07b17ca5dcee9da5ac74618130fd337", size = 1769286 }, + { url = "https://files.pythonhosted.org/packages/12/4f/ad08585b5c8a33c82ea119494c4d3023f4796958c56e668b15cc282ec0a0/unstructured-0.16.25-py3-none-any.whl", hash = "sha256:14719ccef2830216cf1c5bf654f75e2bf07b17ca5dcee9da5ac74618130fd337", size = 1769286, upload-time = "2025-03-07T11:19:37.299Z" }, ] [package.optional-dependencies] @@ -6122,7 +6203,7 @@ pptx = [ [[package]] name = "unstructured-client" -version = "0.36.0" +version = "0.38.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -6133,9 +6214,9 @@ dependencies = [ { name = "pypdf" }, { name = "requests-toolbelt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/4d/d829dbef1138251de771cd52b277d93fb1c4e79d56be3e44e6d2ce76bd62/unstructured_client-0.36.0.tar.gz", hash = "sha256:ab293498100275c0e1d74c926c82dae2b3ba3fbb88945c0ba03b4b7a29197e4a", size = 86010 } +sdist = { url = "https://files.pythonhosted.org/packages/85/60/412092671bfc4952640739f2c0c9b2f4c8af26a3c921738fd12621b4ddd8/unstructured_client-0.38.1.tar.gz", hash = "sha256:43ab0670dd8ff53d71e74f9b6dfe490a84a5303dab80a4873e118a840c6d46ca", size = 91781, upload-time = "2025-07-03T15:46:35.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/4a/ae162e583bbdd0996f92ad18871a737d710260d8c0cfd78f1be6aa0ac150/unstructured_client-0.36.0-py3-none-any.whl", hash = "sha256:d0ecf3ac4d481437d858147904ff6e41205032cf8353af5cdd3ebaa190481d6a", size = 195765 }, + { url = "https://files.pythonhosted.org/packages/26/e0/8c249f00ba85fb4aba5c541463312befbfbf491105ff5c06e508089467be/unstructured_client-0.38.1-py3-none-any.whl", hash = "sha256:71e5467870d0a0119c788c29ec8baf5c0f7123f424affc9d6682eeeb7b8d45fa", size = 212626, upload-time = "2025-07-03T15:46:33.929Z" }, ] [[package]] @@ -6145,70 +6226,49 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/a6/a9178fef247687917701a60eb66542eb5361c58af40c033ba8174ff7366d/upstash_vector-0.6.0.tar.gz", hash = "sha256:a716ed4d0251362208518db8b194158a616d37d1ccbb1155f619df690599e39b", size = 15075 } +sdist = { url = "https://files.pythonhosted.org/packages/94/a6/a9178fef247687917701a60eb66542eb5361c58af40c033ba8174ff7366d/upstash_vector-0.6.0.tar.gz", hash = "sha256:a716ed4d0251362208518db8b194158a616d37d1ccbb1155f619df690599e39b", size = 15075, upload-time = "2024-09-27T12:02:13.533Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/45/95073b83b7fd7b83f10ea314f197bae3989bfe022e736b90145fe9ea4362/upstash_vector-0.6.0-py3-none-any.whl", hash = "sha256:d0bdad7765b8a7f5c205b7a9c81ca4b9a4cee3ee4952afc7d5ea5fb76c3f3c3c", size = 15061 }, + { url = "https://files.pythonhosted.org/packages/5d/45/95073b83b7fd7b83f10ea314f197bae3989bfe022e736b90145fe9ea4362/upstash_vector-0.6.0-py3-none-any.whl", hash = "sha256:d0bdad7765b8a7f5c205b7a9c81ca4b9a4cee3ee4952afc7d5ea5fb76c3f3c3c", size = 15061, upload-time = "2024-09-27T12:02:12.041Z" }, ] [[package]] name = "uritemplate" version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267 } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488 }, + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, ] [[package]] name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, -] - -[[package]] -name = "uuid-utils" -version = "0.11.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/7d83b937889d65682d95b40c94ba226b353d3f532290ee3acb17c8746e49/uuid_utils-0.11.0.tar.gz", hash = "sha256:18cf2b7083da7f3cca0517647213129eb16d20d7ed0dd74b3f4f8bff2aa334ea", size = 18854 } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/20/4a34f2a6e77b1f0f3334b111e4d2411fc8646ab2987892a36507e2d6a498/uuid_utils-0.11.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:094445ccd323bc5507e28e9d6d86b983513efcf19ab59c2dd75239cef765631a", size = 593779 }, - { url = "https://files.pythonhosted.org/packages/a1/a1/1897cd3d37144f698392ec8aae89da2c00c6d34acd77f75312477f4510ab/uuid_utils-0.11.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6430b53d343215f85269ffd74e1d1f4b25ae1031acf0ac24ff3d5721f6a06f48", size = 300848 }, - { url = "https://files.pythonhosted.org/packages/d4/36/3ae8896de8a5320a9e7529452ed29af0082daf8c3787f17c5cbf9defc651/uuid_utils-0.11.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be2e6e4318d23195887fa74fa1d64565a34f7127fdcf22918954981d79765f68", size = 336053 }, - { url = "https://files.pythonhosted.org/packages/fe/b6/751e84cd056074a40ca9ac21db6ca4802e31d78207309c0d9c8ff69cd43b/uuid_utils-0.11.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d37289ab72aa30b5550bfa64d91431c62c89e4969bdf989988aa97f918d5f803", size = 338529 }, - { url = "https://files.pythonhosted.org/packages/3b/c2/f6a1c00a1b067a886fc57c24da46bb0bcb753c92afb898871c6df3ae606f/uuid_utils-0.11.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1012595220f945fe09641f1365a8a06915bf432cac1b31ebd262944934a9b787", size = 480378 }, - { url = "https://files.pythonhosted.org/packages/60/ea/cefc0521e07a35e85416d145382ac4817957cdec037271d0c9e27cbc7d45/uuid_utils-0.11.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35cd3fc718a673e4516e87afb9325558969eca513aa734515b9031d1b651bbb1", size = 332220 }, - { url = "https://files.pythonhosted.org/packages/03/91/5929f209bd4660a7e3b4d47d26189d3cf33e14297312a5f51f5451805fec/uuid_utils-0.11.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed325e0c40e0f59ae82b347f534df954b50cedf12bf60d025625538530e1965d", size = 359052 }, - { url = "https://files.pythonhosted.org/packages/d8/0d/32034d5b13bc07dd95f23122cb743b4eeca8e6d88173ea3c7100c67b6269/uuid_utils-0.11.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5c8b7cf201990ee3140956e541967bd556a7365ec738cb504b04187ad89c757a", size = 515186 }, - { url = "https://files.pythonhosted.org/packages/e7/43/ccf2474f723d6de5e214c22999ffb34219acf83d1e3fff6a4734172e10c0/uuid_utils-0.11.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9966df55bed5d538ba2e9cc40115796480f437f9007727116ef99dc2f42bd5fa", size = 535318 }, - { url = "https://files.pythonhosted.org/packages/fb/05/f668b4ad2b3542cd021c4b27d1ff4e425f854f299bcf7ee36f304399a58c/uuid_utils-0.11.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb04b6c604968424b7e6398d54debbdd5b771b39fc1e648c6eabf3f1dc20582e", size = 502691 }, - { url = "https://files.pythonhosted.org/packages/9e/0b/b906301638eef837c89b19206989dbe27794c591d794ecc06167d9a47c41/uuid_utils-0.11.0-cp39-abi3-win32.whl", hash = "sha256:18420eb3316bb514f09f2da15750ac135478c3a12a704e2c5fb59eab642bb255", size = 180147 }, - { url = "https://files.pythonhosted.org/packages/56/99/ad24ee5ecfc5fbd4a4490bb59c0e72ce604d5eef08683d345546ff6a6f2d/uuid_utils-0.11.0-cp39-abi3-win_amd64.whl", hash = "sha256:37c4805af61a7cce899597d34e7c3dd5cb6a8b4b93a90fbca3826b071ba544df", size = 183574 }, - { url = "https://files.pythonhosted.org/packages/0e/76/2301b1d34defc8c234596ffb6e6d456cd7ef061d108e10a14ceda5ec5d4b/uuid_utils-0.11.0-cp39-abi3-win_arm64.whl", hash = "sha256:4065cf17bbe97f6d8ccc7dc6a0bae7d28fd4797d7f32028a5abd979aeb7bf7c9", size = 181014 }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] name = "uuid6" -version = "2024.7.10" +version = "2025.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/56/2560a9f1ccab9e12b1b3478a3c870796cf4d8ee5652bb19b61751cced14a/uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0", size = 8705 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/3e/4ae6af487ce5781ed71d5fe10aca72e7cbc4d4f45afc31b120287082a8dd/uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7", size = 6376 }, + { url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" }, ] [[package]] name = "uvicorn" -version = "0.34.3" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] [package.optional-dependencies] @@ -6226,38 +6286,38 @@ standard = [ name = "uvloop" version = "0.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, ] [[package]] name = "validators" version = "0.35.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399 } +sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712 }, + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, ] [[package]] name = "vine" version = "5.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636 }, + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, ] [[package]] @@ -6273,93 +6333,94 @@ dependencies = [ { name = "retry" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/62f2fbf0359b31d4e8f766e9ee3096c23d08fc294df1ab6ac117c2d1440c/volcengine_compat-1.0.156.tar.gz", hash = "sha256:e357d096828e31a202dc6047bbc5bf6fff3f54a98cd35a99ab5f965ea741a267", size = 329616 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/62f2fbf0359b31d4e8f766e9ee3096c23d08fc294df1ab6ac117c2d1440c/volcengine_compat-1.0.156.tar.gz", hash = "sha256:e357d096828e31a202dc6047bbc5bf6fff3f54a98cd35a99ab5f965ea741a267", size = 329616, upload-time = "2024-10-13T09:19:09.149Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/da/7ccbe82470dc27e1cfd0466dc637248be906eb8447c28a40c1c74cf617ee/volcengine_compat-1.0.156-py3-none-any.whl", hash = "sha256:4abc149a7601ebad8fa2d28fab50c7945145cf74daecb71bca797b0bdc82c5a5", size = 677272 }, + { url = "https://files.pythonhosted.org/packages/37/da/7ccbe82470dc27e1cfd0466dc637248be906eb8447c28a40c1c74cf617ee/volcengine_compat-1.0.156-py3-none-any.whl", hash = "sha256:4abc149a7601ebad8fa2d28fab50c7945145cf74daecb71bca797b0bdc82c5a5", size = 677272, upload-time = "2024-10-13T09:17:19.944Z" }, ] [[package]] name = "wandb" -version = "0.19.11" +version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "docker-pycreds" }, { name = "gitpython" }, + { name = "packaging" }, { name = "platformdirs" }, { name = "protobuf" }, - { name = "psutil" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "requests" }, { name = "sentry-sdk" }, - { name = "setproctitle" }, - { name = "setuptools" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/98/0ff2925a21b998d4b84731429f4554ca3d9b5cad42c09c075e7306c3aca0/wandb-0.19.11.tar.gz", hash = "sha256:3f50a27dfadbb25946a513ffe856c0e8e538b5626ef207aa50b00c3b0356bff8", size = 39511477 } +sdist = { url = "https://files.pythonhosted.org/packages/73/09/c84264a219e20efd615e4d5d150cc7d359d57d51328d3fa94ee02d70ed9c/wandb-0.21.0.tar.gz", hash = "sha256:473e01ef200b59d780416062991effa7349a34e51425d4be5ff482af2dc39e02", size = 40085784, upload-time = "2025-07-02T00:24:15.516Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/2c/f8bab58c73fdde4442f1baffd9ea5d1bb3113906a97a27e8d9ab72db7a69/wandb-0.19.11-py3-none-any.whl", hash = "sha256:ff3bf050ba25ebae7aedc9a775ffab90c28068832edfe5458423f488c2558f82", size = 6481327 }, - { url = "https://files.pythonhosted.org/packages/45/4a/34b364280f690f4c6d7660f528fba9f13bdecabc4c869d266a4632cf836e/wandb-0.19.11-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:0823fd9aa6343f40c04e01959997ca8c6d6adf1bd81c8d45261fa4915f1c6b67", size = 20555751 }, - { url = "https://files.pythonhosted.org/packages/d8/e6/a27868fdb83a60df37b9d15e52c3353dd88d74442f27ae48cf765c6b9554/wandb-0.19.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c758ef5439599d9023db5b3cf1698477055d82f9fae48af2779f63f1d289167c", size = 20377587 }, - { url = "https://files.pythonhosted.org/packages/21/f7/d5cf5b58c2b3015364c7b2b6af6a440cbeda4103b67332e1e64b30f6252d/wandb-0.19.11-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:de2dfd4911e7691735e271654c735e7b90cdee9d29a3796fbf06e9e92d48f3d7", size = 20985041 }, - { url = "https://files.pythonhosted.org/packages/68/06/8b827f16a0b8f18002d2fffa7c5a7fd447946e0d0c68aeec0dd7eb18cdd3/wandb-0.19.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfff738850770d26b13f8f3fe400a6456f1e39e87f3f29d5aa241b249476df95", size = 20017696 }, - { url = "https://files.pythonhosted.org/packages/f9/31/eeb2878b26566c04c3e9b8b20b3ec3c54a2be50535088d36a37c008e07a3/wandb-0.19.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ff673007448df11cc69379ae0df28ead866800dc1ec7bc151b402db0bbcf40", size = 21425857 }, - { url = "https://files.pythonhosted.org/packages/10/30/08988360678ae78334bb16625c28260fcaba49f500b89f8766807cb74d71/wandb-0.19.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:858bc5023fa1b3285d89d15f62be78afdb28301064daa49ea3f4ebde5dcedad2", size = 20023145 }, - { url = "https://files.pythonhosted.org/packages/c8/e9/a639c42c8ca517c4d25e8970d64d0c5a9bd35b784faed5f47d9cca3dcd12/wandb-0.19.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90e4b57649896acb16c3dd41b3093df1a169c2f1d94ff15d76af86b8a60dcdac", size = 21504842 }, - { url = "https://files.pythonhosted.org/packages/44/74/dbe9277dd935b77dd16939cdf15357766fec0813a6e336cf5f1d07eb016e/wandb-0.19.11-py3-none-win32.whl", hash = "sha256:38dea43c7926d8800405a73b80b9adfe81eb315fc6f2ac6885c77eb966634421", size = 20767584 }, - { url = "https://files.pythonhosted.org/packages/36/d5/215cac3edec5c5ac6e7231beb9d22466d5d4e4a132fa3a1d044f7d682c15/wandb-0.19.11-py3-none-win_amd64.whl", hash = "sha256:73402003c56ddc2198878492ab2bff55bb49bce5587eae5960e737d27c0c48f7", size = 20767588 }, + { url = "https://files.pythonhosted.org/packages/38/dd/65eac086e1bc337bb5f0eed65ba1fe4a6dbc62c97f094e8e9df1ef83ffed/wandb-0.21.0-py3-none-any.whl", hash = "sha256:316e8cd4329738f7562f7369e6eabeeb28ef9d473203f7ead0d03e5dba01c90d", size = 6504284, upload-time = "2025-07-02T00:23:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/80556ce9097f59e10807aa68f4a9b29d736a90dca60852a9e2af1641baf8/wandb-0.21.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:701d9cbdfcc8550a330c1b54a26f1585519180e0f19247867446593d34ace46b", size = 21717388, upload-time = "2025-07-02T00:23:49.348Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/660bc75aa37bd23409822ea5ed616177d94873172d34271693c80405c820/wandb-0.21.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:01689faa6b691df23ba2367e0a1ecf6e4d0be44474905840098eedd1fbcb8bdf", size = 21141465, upload-time = "2025-07-02T00:23:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/9861929530be56557c74002868c85d0d8ac57050cc21863afe909ae3d46f/wandb-0.21.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:55d3f42ddb7971d1699752dff2b85bcb5906ad098d18ab62846c82e9ce5a238d", size = 21793511, upload-time = "2025-07-02T00:23:55.447Z" }, + { url = "https://files.pythonhosted.org/packages/de/52/e5cad2eff6fbed1ac06f4a5b718457fa2fd437f84f5c8f0d31995a2ef046/wandb-0.21.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:893508f0c7da48917448daa5cd622c27ce7ce15119adaa861185034c2bd7b14c", size = 20704643, upload-time = "2025-07-02T00:23:58.255Z" }, + { url = "https://files.pythonhosted.org/packages/83/8f/6bed9358cc33767c877b221d4f565e1ddf00caf4bbbe54d2e3bbc932c6a7/wandb-0.21.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4e8245a8912247ddf7654f7b5330f583a6c56ab88fee65589158490d583c57d", size = 22243012, upload-time = "2025-07-02T00:24:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/9048015412ea5ca916844af55add4fed7c21fe1ad70bb137951e70b550c5/wandb-0.21.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c4f951e0d02755e315679bfdcb5bc38c1b02e2e5abc5432b91a91bb0cf246", size = 20716440, upload-time = "2025-07-02T00:24:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/02/d9/fcd2273d8ec3f79323e40a031aba5d32d6fa9065702010eb428b5ffbab62/wandb-0.21.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:873749966eeac0069e0e742e6210641b6227d454fb1dae2cf5c437c6ed42d3ca", size = 22320652, upload-time = "2025-07-02T00:24:07.175Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/b8308db6b9c3c96dcd03be17c019aee105e1d7dc1e74d70756cdfb9241c6/wandb-0.21.0-py3-none-win32.whl", hash = "sha256:9d3cccfba658fa011d6cab9045fa4f070a444885e8902ae863802549106a5dab", size = 21484296, upload-time = "2025-07-02T00:24:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/71cc033e8abd00e54465e68764709ed945e2da2d66d764f72f4660262b22/wandb-0.21.0-py3-none-win_amd64.whl", hash = "sha256:28a0b2dad09d7c7344ac62b0276be18a2492a5578e4d7c84937a3e1991edaac7", size = 21484301, upload-time = "2025-07-02T00:24:12.658Z" }, ] [[package]] name = "watchfiles" -version = "1.0.5" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/f4/41b591f59021786ef517e1cdc3b510383551846703e03f204827854a96f8/watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827", size = 405336 }, - { url = "https://files.pythonhosted.org/packages/ae/06/93789c135be4d6d0e4f63e96eea56dc54050b243eacc28439a26482b5235/watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4", size = 395977 }, - { url = "https://files.pythonhosted.org/packages/d2/db/1cd89bd83728ca37054512d4d35ab69b5f12b8aa2ac9be3b0276b3bf06cc/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d", size = 455232 }, - { url = "https://files.pythonhosted.org/packages/40/90/d8a4d44ffe960517e487c9c04f77b06b8abf05eb680bed71c82b5f2cad62/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63", size = 459151 }, - { url = "https://files.pythonhosted.org/packages/6c/da/267a1546f26465dead1719caaba3ce660657f83c9d9c052ba98fb8856e13/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418", size = 489054 }, - { url = "https://files.pythonhosted.org/packages/b1/31/33850dfd5c6efb6f27d2465cc4c6b27c5a6f5ed53c6fa63b7263cf5f60f6/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9", size = 523955 }, - { url = "https://files.pythonhosted.org/packages/09/84/b7d7b67856efb183a421f1416b44ca975cb2ea6c4544827955dfb01f7dc2/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6", size = 502234 }, - { url = "https://files.pythonhosted.org/packages/71/87/6dc5ec6882a2254cfdd8b0718b684504e737273903b65d7338efaba08b52/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25", size = 454750 }, - { url = "https://files.pythonhosted.org/packages/3d/6c/3786c50213451a0ad15170d091570d4a6554976cf0df19878002fc96075a/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5", size = 631591 }, - { url = "https://files.pythonhosted.org/packages/1b/b3/1427425ade4e359a0deacce01a47a26024b2ccdb53098f9d64d497f6684c/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01", size = 625370 }, - { url = "https://files.pythonhosted.org/packages/15/ba/f60e053b0b5b8145d682672024aa91370a29c5c921a88977eb565de34086/watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246", size = 277791 }, - { url = "https://files.pythonhosted.org/packages/50/ed/7603c4e164225c12c0d4e8700b64bb00e01a6c4eeea372292a3856be33a4/watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096", size = 291622 }, - { url = "https://files.pythonhosted.org/packages/a2/c2/99bb7c96b4450e36877fde33690ded286ff555b5a5c1d925855d556968a1/watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed", size = 283699 }, - { url = "https://files.pythonhosted.org/packages/2a/8c/4f0b9bdb75a1bfbd9c78fad7d8854369283f74fe7cf03eb16be77054536d/watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2", size = 401511 }, - { url = "https://files.pythonhosted.org/packages/dc/4e/7e15825def77f8bd359b6d3f379f0c9dac4eb09dd4ddd58fd7d14127179c/watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f", size = 392715 }, - { url = "https://files.pythonhosted.org/packages/58/65/b72fb817518728e08de5840d5d38571466c1b4a3f724d190cec909ee6f3f/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec", size = 454138 }, - { url = "https://files.pythonhosted.org/packages/3e/a4/86833fd2ea2e50ae28989f5950b5c3f91022d67092bfec08f8300d8b347b/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21", size = 458592 }, - { url = "https://files.pythonhosted.org/packages/38/7e/42cb8df8be9a37e50dd3a818816501cf7a20d635d76d6bd65aae3dbbff68/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512", size = 487532 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/13d26721c85d7f3df6169d8b495fcac8ab0dc8f0945ebea8845de4681dab/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d", size = 522865 }, - { url = "https://files.pythonhosted.org/packages/a1/0d/7f9ae243c04e96c5455d111e21b09087d0eeaf9a1369e13a01c7d3d82478/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6", size = 499887 }, - { url = "https://files.pythonhosted.org/packages/8e/0f/a257766998e26aca4b3acf2ae97dff04b57071e991a510857d3799247c67/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234", size = 454498 }, - { url = "https://files.pythonhosted.org/packages/81/79/8bf142575a03e0af9c3d5f8bcae911ee6683ae93a625d349d4ecf4c8f7df/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2", size = 630663 }, - { url = "https://files.pythonhosted.org/packages/f1/80/abe2e79f610e45c63a70d271caea90c49bbf93eb00fa947fa9b803a1d51f/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663", size = 625410 }, - { url = "https://files.pythonhosted.org/packages/91/6f/bc7fbecb84a41a9069c2c6eb6319f7f7df113adf113e358c57fc1aff7ff5/watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249", size = 277965 }, - { url = "https://files.pythonhosted.org/packages/99/a5/bf1c297ea6649ec59e935ab311f63d8af5faa8f0b86993e3282b984263e3/watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705", size = 291693 }, - { url = "https://files.pythonhosted.org/packages/7f/7b/fd01087cc21db5c47e5beae507b87965db341cce8a86f9eb12bf5219d4e0/watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417", size = 283287 }, +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751, upload-time = "2025-06-15T19:05:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313, upload-time = "2025-06-15T19:05:08.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792, upload-time = "2025-06-15T19:05:09.869Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196, upload-time = "2025-06-15T19:05:11.91Z" }, + { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788, upload-time = "2025-06-15T19:05:13.373Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879, upload-time = "2025-06-15T19:05:14.725Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447, upload-time = "2025-06-15T19:05:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145, upload-time = "2025-06-15T19:05:17.17Z" }, + { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539, upload-time = "2025-06-15T19:05:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472, upload-time = "2025-06-15T19:05:19.588Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348, upload-time = "2025-06-15T19:05:20.856Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607, upload-time = "2025-06-15T19:05:21.937Z" }, + { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056, upload-time = "2025-06-15T19:05:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910, upload-time = "2025-06-15T19:06:49.335Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816, upload-time = "2025-06-15T19:06:50.433Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584, upload-time = "2025-06-15T19:06:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009, upload-time = "2025-06-15T19:06:52.896Z" }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] [[package]] name = "weave" -version = "0.51.50" +version = "0.51.54" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -6373,12 +6434,11 @@ dependencies = [ { name = "pydantic" }, { name = "rich" }, { name = "tenacity" }, - { name = "uuid-utils" }, { name = "wandb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/d1/47ab7923eb389ec7b1ca0138d9929dc50bfd0dfac324f42167f99fa02798/weave-0.51.50.tar.gz", hash = "sha256:773434765a3230bf8f4dfe9e04f9c7dfd90b03f18bb5e069186ce67d1f7c4dd8", size = 410739 } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2b/bdac08ae2fa7f660e3fb02e9f4acec5a5683509decd8fbd1ad5641160d3a/weave-0.51.54.tar.gz", hash = "sha256:41aaaa770c0ac2259325dd6035e1bf96f47fb92dbd4eec54d3ef4847587cc061", size = 425873, upload-time = "2025-06-16T21:57:47.582Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/4a/72ed6f8435759f44090c8ae81d3a50716f0c3e527e733a78e77b9a834372/weave-0.51.50-py3-none-any.whl", hash = "sha256:23fb74ec95f57fe30f31019f32f6626f39993aa228024eb9cf8fe83e58b698de", size = 524057 }, + { url = "https://files.pythonhosted.org/packages/48/4d/7cee23e5bf5faab149aeb7cca367a434c4aec1fa0cb1f5a1d20149a2bf6f/weave-0.51.54-py3-none-any.whl", hash = "sha256:7de2c0da8061bc007de2f74fb3dd2496d24337dff3723f057be49fcf53e0a3a2", size = 542168, upload-time = "2025-06-16T21:57:44.929Z" }, ] [[package]] @@ -6390,67 +6450,67 @@ dependencies = [ { name = "requests" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/c1/3285a21d8885f2b09aabb65edb9a8e062a35c2d7175e1bb024fa096582ab/weaviate-client-3.24.2.tar.gz", hash = "sha256:6914c48c9a7e5ad0be9399271f9cb85d6f59ab77476c6d4e56a3925bf149edaa", size = 199332 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/c1/3285a21d8885f2b09aabb65edb9a8e062a35c2d7175e1bb024fa096582ab/weaviate-client-3.24.2.tar.gz", hash = "sha256:6914c48c9a7e5ad0be9399271f9cb85d6f59ab77476c6d4e56a3925bf149edaa", size = 199332, upload-time = "2023-10-04T08:37:54.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/98/3136d05f93e30cf29e1db280eaadf766df18d812dfe7994bcced653b2340/weaviate_client-3.24.2-py3-none-any.whl", hash = "sha256:bc50ca5fcebcd48de0d00f66700b0cf7c31a97c4cd3d29b4036d77c5d1d9479b", size = 107968 }, + { url = "https://files.pythonhosted.org/packages/ab/98/3136d05f93e30cf29e1db280eaadf766df18d812dfe7994bcced653b2340/weaviate_client-3.24.2-py3-none-any.whl", hash = "sha256:bc50ca5fcebcd48de0d00f66700b0cf7c31a97c4cd3d29b4036d77c5d1d9479b", size = 107968, upload-time = "2023-10-04T08:37:52.511Z" }, ] [[package]] name = "webencodings" version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] [[package]] name = "websocket-client" version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "webvtt-py" version = "0.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/7c9c964681fb148e0293e6860108d378e09ccab2218f9063fd3eb87f840a/webvtt-py-0.5.1.tar.gz", hash = "sha256:2040dd325277ddadc1e0c6cc66cbc4a1d9b6b49b24c57a0c3364374c3e8a3dc1", size = 55128 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/7c9c964681fb148e0293e6860108d378e09ccab2218f9063fd3eb87f840a/webvtt-py-0.5.1.tar.gz", hash = "sha256:2040dd325277ddadc1e0c6cc66cbc4a1d9b6b49b24c57a0c3364374c3e8a3dc1", size = 55128, upload-time = "2024-05-30T13:40:17.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/ed/aad7e0f5a462d679f7b4d2e0d8502c3096740c883b5bbed5103146480937/webvtt_py-0.5.1-py3-none-any.whl", hash = "sha256:9d517d286cfe7fc7825e9d4e2079647ce32f5678eb58e39ef544ffbb932610b7", size = 19802 }, + { url = "https://files.pythonhosted.org/packages/f3/ed/aad7e0f5a462d679f7b4d2e0d8502c3096740c883b5bbed5103146480937/webvtt_py-0.5.1-py3-none-any.whl", hash = "sha256:9d517d286cfe7fc7825e9d4e2079647ce32f5678eb58e39ef544ffbb932610b7", size = 19802, upload-time = "2024-05-30T13:40:14.661Z" }, ] [[package]] @@ -6460,40 +6520,40 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, ] [[package]] name = "wrapt" version = "1.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, ] [[package]] @@ -6505,36 +6565,36 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/cf/7f825a311b11d1e0f7947a94f88adcf1d31e707c54a6d76d61a5d98604ed/xinference-client-1.2.2.tar.gz", hash = "sha256:85d2ba0fcbaae616b06719c422364123cbac97f3e3c82e614095fe6d0e630ed0", size = 44824 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/cf/7f825a311b11d1e0f7947a94f88adcf1d31e707c54a6d76d61a5d98604ed/xinference-client-1.2.2.tar.gz", hash = "sha256:85d2ba0fcbaae616b06719c422364123cbac97f3e3c82e614095fe6d0e630ed0", size = 44824, upload-time = "2025-02-08T09:28:56.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/0f/fc58e062cf2f7506a33d2fe5446a1e88eb7f64914addffd7ed8b12749712/xinference_client-1.2.2-py3-none-any.whl", hash = "sha256:6941d87cf61283a9d6e81cee6cb2609a183d34c6b7d808c6ba0c33437520518f", size = 25723 }, + { url = "https://files.pythonhosted.org/packages/77/0f/fc58e062cf2f7506a33d2fe5446a1e88eb7f64914addffd7ed8b12749712/xinference_client-1.2.2-py3-none-any.whl", hash = "sha256:6941d87cf61283a9d6e81cee6cb2609a183d34c6b7d808c6ba0c33437520518f", size = 25723, upload-time = "2025-02-08T09:28:54.046Z" }, ] [[package]] name = "xlrd" -version = "2.0.1" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/b3/19a2540d21dea5f908304375bd43f5ed7a4c28a370dc9122c565423e6b44/xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88", size = 100259 } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/0c/c2a72d51fe56e08a08acc85d13013558a2d793028ae7385448a6ccdfae64/xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd", size = 96531 }, + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, ] [[package]] name = "xlsxwriter" -version = "3.2.3" +version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/d1/e026d33dd5d552e5bf3a873dee54dad66b550230df8290d79394f09b2315/xlsxwriter-3.2.3.tar.gz", hash = "sha256:ad6fd41bdcf1b885876b1f6b7087560aecc9ae5a9cc2ba97dcac7ab2e210d3d5", size = 209135 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload-time = "2025-06-17T08:59:14.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/b1/a252d499f2760b314fcf264d2b36fcc4343a1ecdb25492b210cb0db70a68/XlsxWriter-3.2.3-py3-none-any.whl", hash = "sha256:593f8296e8a91790c6d0378ab08b064f34a642b3feb787cf6738236bd0a4860d", size = 169433 }, + { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload-time = "2025-06-17T08:59:13.453Z" }, ] [[package]] name = "xmltodict" version = "0.14.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942 } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942, upload-time = "2024-10-16T06:10:29.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981 }, + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981, upload-time = "2024-10-16T06:10:27.649Z" }, ] [[package]] @@ -6546,62 +6606,62 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, - { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, - { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, - { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, - { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, - { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, - { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, - { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, - { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, - { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, - { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, - { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, - { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, - { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, - { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, - { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, - { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, - { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, - { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, - { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, - { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, - { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, - { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, - { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, - { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, - { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, - { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, - { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, - { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, - { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, - { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062, upload-time = "2024-12-01T20:35:23.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555, upload-time = "2024-12-01T20:33:08.819Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351, upload-time = "2024-12-01T20:33:10.609Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286, upload-time = "2024-12-01T20:33:12.322Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649, upload-time = "2024-12-01T20:33:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623, upload-time = "2024-12-01T20:33:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007, upload-time = "2024-12-01T20:33:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145, upload-time = "2024-12-01T20:33:20.071Z" }, + { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133, upload-time = "2024-12-01T20:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967, upload-time = "2024-12-01T20:33:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397, upload-time = "2024-12-01T20:33:26.205Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206, upload-time = "2024-12-01T20:33:27.83Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089, upload-time = "2024-12-01T20:33:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267, upload-time = "2024-12-01T20:33:31.449Z" }, + { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141, upload-time = "2024-12-01T20:33:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402, upload-time = "2024-12-01T20:33:35.689Z" }, + { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030, upload-time = "2024-12-01T20:33:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644, upload-time = "2024-12-01T20:33:39.204Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962, upload-time = "2024-12-01T20:33:40.808Z" }, + { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795, upload-time = "2024-12-01T20:33:42.322Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368, upload-time = "2024-12-01T20:33:43.956Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314, upload-time = "2024-12-01T20:33:46.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987, upload-time = "2024-12-01T20:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914, upload-time = "2024-12-01T20:33:50.875Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765, upload-time = "2024-12-01T20:33:52.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444, upload-time = "2024-12-01T20:33:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760, upload-time = "2024-12-01T20:33:56.286Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484, upload-time = "2024-12-01T20:33:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864, upload-time = "2024-12-01T20:34:00.22Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537, upload-time = "2024-12-01T20:34:03.54Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861, upload-time = "2024-12-01T20:34:05.73Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097, upload-time = "2024-12-01T20:34:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399, upload-time = "2024-12-01T20:34:09.61Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109, upload-time = "2024-12-01T20:35:20.834Z" }, ] [[package]] name = "zipp" -version = "3.22.0" +version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257 } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796 }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] [[package]] name = "zope-event" -version = "5.0" +version = "5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/c7/31e6f40282a2c548602c177826df281177caf79efaa101dd14314fb4ee73/zope_event-5.1.tar.gz", hash = "sha256:a153660e0c228124655748e990396b9d8295d6e4f546fa1b34f3319e1c666e7f", size = 18632, upload-time = "2025-06-26T07:14:22.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 }, + { url = "https://files.pythonhosted.org/packages/00/ed/d8c3f56c1edb0ee9b51461dd08580382e9589850f769b69f0dedccff5215/zope_event-5.1-py3-none-any.whl", hash = "sha256:53de8f0e9f61dc0598141ac591f49b042b6d74784dab49971b9cc91d0f73a7df", size = 6905, upload-time = "2025-06-26T07:14:21.779Z" }, ] [[package]] @@ -6611,20 +6671,20 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } +sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960, upload-time = "2024-11-28T08:45:39.224Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, - { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, - { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, - { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, - { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, - { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, - { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, - { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, - { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, - { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, + { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776, upload-time = "2024-11-28T08:47:53.009Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296, upload-time = "2024-11-28T08:47:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997, upload-time = "2024-11-28T09:18:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038, upload-time = "2024-11-28T08:48:26.381Z" }, + { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806, upload-time = "2024-11-28T08:48:30.78Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305, upload-time = "2024-11-28T08:49:14.525Z" }, + { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959, upload-time = "2024-11-28T08:47:47.788Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357, upload-time = "2024-11-28T08:47:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235, upload-time = "2024-11-28T09:18:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253, upload-time = "2024-11-28T08:48:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702, upload-time = "2024-11-28T08:48:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" }, ] [[package]] @@ -6634,40 +6694,40 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699 }, - { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681 }, - { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328 }, - { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955 }, - { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944 }, - { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927 }, - { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910 }, - { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544 }, - { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094 }, - { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440 }, - { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091 }, - { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682 }, - { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707 }, - { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792 }, - { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586 }, - { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420 }, - { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713 }, - { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459 }, - { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707 }, - { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545 }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533 }, - { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510 }, - { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973 }, - { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968 }, - { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179 }, - { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577 }, - { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899 }, - { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964 }, - { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398 }, - { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313 }, - { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877 }, - { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595 }, +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, ] [package.optional-dependencies] diff --git a/dev/mypy-check b/dev/mypy-check index c043faffe6..8a2342730c 100755 --- a/dev/mypy-check +++ b/dev/mypy-check @@ -7,4 +7,4 @@ cd "$SCRIPT_DIR/.." # run mypy checks uv run --directory api --dev --with pip \ - python -m mypy --install-types --non-interactive --cache-fine-grained --sqlite-cache . + python -m mypy --install-types --non-interactive --exclude venv ./ diff --git a/docker/.env.example b/docker/.env.example index 4cf5e202d0..e08a81e49e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -47,6 +47,11 @@ APP_WEB_URL= # ensuring port 5001 is externally accessible (see docker-compose.yaml). FILES_URL= +# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network. +# Set this to the internal Docker service URL for proper plugin file access. +# Example: INTERNAL_FILES_URL=http://api:5001 +INTERNAL_FILES_URL= + # ------------------------------ # Server Configuration # ------------------------------ @@ -209,6 +214,10 @@ SQLALCHEMY_POOL_SIZE=30 SQLALCHEMY_POOL_RECYCLE=3600 # Whether to print SQL, default is false. SQLALCHEMY_ECHO=false +# If True, will test connections for liveness upon each checkout +SQLALCHEMY_POOL_PRE_PING=false +# Whether to enable the Last in first out option or use default FIFO queue if is false +SQLALCHEMY_POOL_USE_LIFO=false # Maximum number of connections to the database # Default is 100 @@ -285,6 +294,7 @@ BROKER_USE_SSL=false # If you are using Redis Sentinel for high availability, configure the following settings. CELERY_USE_SENTINEL=false CELERY_SENTINEL_MASTER_NAME= +CELERY_SENTINEL_PASSWORD= CELERY_SENTINEL_SOCKET_TIMEOUT=0.1 # ------------------------------ @@ -399,7 +409,7 @@ SUPABASE_URL=your-server-url # ------------------------------ # The type of vector store to use. -# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`. +# Supported values are `weaviate`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `oceanbase`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`. VECTOR_STORE=weaviate # The Weaviate endpoint URL. Only available when VECTOR_STORE is `weaviate`. @@ -490,6 +500,13 @@ TIDB_VECTOR_USER= TIDB_VECTOR_PASSWORD= TIDB_VECTOR_DATABASE=dify +# Matrixone vector configurations. +MATRIXONE_HOST=matrixone +MATRIXONE_PORT=6001 +MATRIXONE_USER=dump +MATRIXONE_PASSWORD=111 +MATRIXONE_DATABASE=dify + # Tidb on qdrant configuration, only available when VECTOR_STORE is `tidb_on_qdrant` TIDB_ON_QDRANT_URL=http://127.0.0.1 TIDB_ON_QDRANT_API_KEY=dify @@ -719,10 +736,11 @@ NOTION_INTERNAL_SECRET= # Mail related configuration # ------------------------------ -# Mail type, support: resend, smtp +# Mail type, support: resend, smtp, sendgrid MAIL_TYPE=resend # Default send from email address, if not specified +# If using SendGrid, use the 'from' field for authentication if necessary. MAIL_DEFAULT_SEND_FROM= # API-Key for the Resend email provider, used when MAIL_TYPE is `resend`. @@ -738,6 +756,9 @@ SMTP_PASSWORD= SMTP_USE_TLS=true SMTP_OPPORTUNISTIC_TLS=false +# Sendgid configuration +SENDGRID_API_KEY= + # ------------------------------ # Others Configuration # ------------------------------ @@ -782,11 +803,27 @@ WORKFLOW_FILE_UPLOAD_LIMIT=10 # hybrid: Save new data to object storage, read from both object storage and RDBMS WORKFLOW_NODE_EXECUTION_STORAGE=rdbms +# Repository configuration +# Core workflow execution repository implementation +CORE_WORKFLOW_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository + +# Core workflow node execution repository implementation +CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY=core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository + +# API workflow node execution repository implementation +API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository + +# API workflow run repository implementation +API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository + # HTTP request node in workflow configuration HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 HTTP_REQUEST_NODE_SSL_VERIFY=True +# Respect X-* headers to redirect clients +RESPECT_XFORWARD_HEADERS_ENABLED=false + # SSRF Proxy server HTTP URL SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128 # SSRF Proxy server HTTPS URL @@ -811,11 +848,15 @@ MAX_ITERATIONS_NUM=99 # The timeout for the text generation in millisecond TEXT_GENERATION_TIMEOUT_MS=60000 +# Allow rendering unsafe URLs which have "data:" scheme. +ALLOW_UNSAFE_DATA_SCHEME=false + # ------------------------------ # Environment Variables for db Service # ------------------------------ -PGUSER=${DB_USERNAME} +# The name of the default postgres user. +POSTGRES_USER=${DB_USERNAME} # The password for the default postgres user. POSTGRES_PASSWORD=${DB_PASSWORD} # The name of the default postgres database. @@ -942,7 +983,7 @@ NGINX_SSL_PROTOCOLS=TLSv1.1 TLSv1.2 TLSv1.3 # Nginx performance tuning NGINX_WORKER_PROCESSES=auto -NGINX_CLIENT_MAX_BODY_SIZE=15M +NGINX_CLIENT_MAX_BODY_SIZE=100M NGINX_KEEPALIVE_TIMEOUT=65 # Proxy settings @@ -1067,6 +1108,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets # Plugin oss bucket PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials +PLUGIN_S3_USE_AWS=false PLUGIN_S3_USE_AWS_MANAGED_IAM=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 75bdab1a06..7c1544acb9 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.1 + image: langgenius/dify-web:1.6.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -67,6 +67,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-} @@ -84,7 +85,7 @@ services: image: postgres:15-alpine restart: always environment: - PGUSER: ${PGUSER:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_DB: ${POSTGRES_DB:-dify} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -142,7 +143,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always environment: # Use the shared environment variables. @@ -168,6 +169,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -264,7 +266,7 @@ services: NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -434,7 +436,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts container_name: oceanbase profiles: - oceanbase @@ -449,9 +451,15 @@ services: OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_SERVER_IP: 127.0.0.1 - MODE: MINI + MODE: mini ports: - "${OCEANBASE_VECTOR_PORT:-2881}:2881" + healthcheck: + test: [ 'CMD-SHELL', 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e "SELECT 1;"' ] + interval: 10s + retries: 30 + start_period: 30s + timeout: 10s # Oracle vector database oracle: @@ -610,6 +618,18 @@ services: ports: - ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123} + # Matrixone vector store. + matrixone: + hostname: matrixone + image: matrixorigin/matrixone:2.1.1 + profiles: + - matrixone + restart: always + volumes: + - ./volumes/matrixone/data:/mo-data + ports: + - ${MATRIXONE_PORT:-6001}:${MATRIXONE_PORT:-6001} + # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites elasticsearch: diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 8276e2977f..0b1885755b 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -71,7 +71,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always env_file: - ./middleware.env @@ -104,6 +104,7 @@ services: PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e559021684..73e061e770 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -11,6 +11,7 @@ x-shared-env: &shared-api-worker-env APP_API_URL: ${APP_API_URL:-} APP_WEB_URL: ${APP_WEB_URL:-} FILES_URL: ${FILES_URL:-} + INTERNAL_FILES_URL: ${INTERNAL_FILES_URL:-} LOG_LEVEL: ${LOG_LEVEL:-INFO} LOG_FILE: ${LOG_FILE:-/app/logs/server.log} LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20} @@ -55,6 +56,8 @@ x-shared-env: &shared-api-worker-env SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} + SQLALCHEMY_POOL_PRE_PING: ${SQLALCHEMY_POOL_PRE_PING:-false} + SQLALCHEMY_POOL_USE_LIFO: ${SQLALCHEMY_POOL_USE_LIFO:-false} POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-128MB} POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB} @@ -79,6 +82,7 @@ x-shared-env: &shared-api-worker-env BROKER_USE_SSL: ${BROKER_USE_SSL:-false} CELERY_USE_SENTINEL: ${CELERY_USE_SENTINEL:-false} CELERY_SENTINEL_MASTER_NAME: ${CELERY_SENTINEL_MASTER_NAME:-} + CELERY_SENTINEL_PASSWORD: ${CELERY_SENTINEL_PASSWORD:-} CELERY_SENTINEL_SOCKET_TIMEOUT: ${CELERY_SENTINEL_SOCKET_TIMEOUT:-0.1} WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} @@ -195,6 +199,11 @@ x-shared-env: &shared-api-worker-env TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-} TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-} TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} + MATRIXONE_HOST: ${MATRIXONE_HOST:-matrixone} + MATRIXONE_PORT: ${MATRIXONE_PORT:-6001} + MATRIXONE_USER: ${MATRIXONE_USER:-dump} + MATRIXONE_PASSWORD: ${MATRIXONE_PASSWORD:-111} + MATRIXONE_DATABASE: ${MATRIXONE_DATABASE:-dify} TIDB_ON_QDRANT_URL: ${TIDB_ON_QDRANT_URL:-http://127.0.0.1} TIDB_ON_QDRANT_API_KEY: ${TIDB_ON_QDRANT_API_KEY:-dify} TIDB_ON_QDRANT_CLIENT_TIMEOUT: ${TIDB_ON_QDRANT_CLIENT_TIMEOUT:-20} @@ -322,6 +331,7 @@ x-shared-env: &shared-api-worker-env SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_USE_TLS: ${SMTP_USE_TLS:-true} SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} + SENDGRID_API_KEY: ${SENDGRID_API_KEY:-} INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-4000} INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} RESET_PASSWORD_TOKEN_EXPIRY_MINUTES: ${RESET_PASSWORD_TOKEN_EXPIRY_MINUTES:-5} @@ -346,9 +356,14 @@ x-shared-env: &shared-api-worker-env WORKFLOW_PARALLEL_DEPTH_LIMIT: ${WORKFLOW_PARALLEL_DEPTH_LIMIT:-3} WORKFLOW_FILE_UPLOAD_LIMIT: ${WORKFLOW_FILE_UPLOAD_LIMIT:-10} WORKFLOW_NODE_EXECUTION_STORAGE: ${WORKFLOW_NODE_EXECUTION_STORAGE:-rdbms} + CORE_WORKFLOW_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_execution_repository.SQLAlchemyWorkflowExecutionRepository} + CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository} + API_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${API_WORKFLOW_NODE_EXECUTION_REPOSITORY:-repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository} + API_WORKFLOW_RUN_REPOSITORY: ${API_WORKFLOW_RUN_REPOSITORY:-repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository} HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} + RESPECT_XFORWARD_HEADERS_ENABLED: ${RESPECT_XFORWARD_HEADERS_ENABLED:-false} SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100} @@ -356,7 +371,8 @@ x-shared-env: &shared-api-worker-env MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10} MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} - PGUSER: ${PGUSER:-${DB_USERNAME}} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} + POSTGRES_USER: ${POSTGRES_USER:-${DB_USERNAME}} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}} POSTGRES_DB: ${POSTGRES_DB:-${DB_DATABASE}} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -412,7 +428,7 @@ x-shared-env: &shared-api-worker-env NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -467,6 +483,7 @@ x-shared-env: &shared-api-worker-env PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + PLUGIN_S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} PLUGIN_S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} PLUGIN_S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} PLUGIN_S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} @@ -508,7 +525,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -537,7 +554,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.4.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -563,7 +580,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.4.1 + image: langgenius/dify-web:1.6.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} @@ -573,6 +590,7 @@ services: TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} CSP_WHITELIST: ${CSP_WHITELIST:-} ALLOW_EMBED: ${ALLOW_EMBED:-false} + ALLOW_UNSAFE_DATA_SCHEME: ${ALLOW_UNSAFE_DATA_SCHEME:-false} MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-https://marketplace.dify.ai} MARKETPLACE_URL: ${MARKETPLACE_URL:-https://marketplace.dify.ai} TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-} @@ -590,7 +608,7 @@ services: image: postgres:15-alpine restart: always environment: - PGUSER: ${PGUSER:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456} POSTGRES_DB: ${POSTGRES_DB:-dify} PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata} @@ -648,7 +666,7 @@ services: # plugin daemon plugin_daemon: - image: langgenius/dify-plugin-daemon:0.1.1-local + image: langgenius/dify-plugin-daemon:0.1.3-local restart: always environment: # Use the shared environment variables. @@ -674,6 +692,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} @@ -770,7 +789,7 @@ services: NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} - NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-100M} NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} @@ -940,7 +959,7 @@ services: # OceanBase vector database oceanbase: - image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818 + image: oceanbase/oceanbase-ce:4.3.5-lts container_name: oceanbase profiles: - oceanbase @@ -955,9 +974,15 @@ services: OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456} OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai} OB_SERVER_IP: 127.0.0.1 - MODE: MINI + MODE: mini ports: - "${OCEANBASE_VECTOR_PORT:-2881}:2881" + healthcheck: + test: [ 'CMD-SHELL', 'obclient -h127.0.0.1 -P2881 -uroot@test -p$${OB_TENANT_PASSWORD} -e "SELECT 1;"' ] + interval: 10s + retries: 30 + start_period: 30s + timeout: 10s # Oracle vector database oracle: @@ -1116,6 +1141,18 @@ services: ports: - ${MYSCALE_PORT:-8123}:${MYSCALE_PORT:-8123} + # Matrixone vector store. + matrixone: + hostname: matrixone + image: matrixorigin/matrixone:2.1.1 + profiles: + - matrixone + restart: always + volumes: + - ./volumes/matrixone/data:/mo-data + ports: + - ${MATRIXONE_PORT:-6001}:${MATRIXONE_PORT:-6001} + # https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-prod-prerequisites elasticsearch: diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 66037f281c..2eba62f594 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -1,7 +1,7 @@ # ------------------------------ # Environment Variables for db Service # ------------------------------ -PGUSER=postgres +POSTGRES_USER=postgres # The password for the default postgres user. POSTGRES_PASSWORD=difyai123456 # The name of the default postgres database. @@ -133,6 +133,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials PLUGIN_S3_USE_AWS_MANAGED_IAM=false +PLUGIN_S3_USE_AWS=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false PLUGIN_AWS_ACCESS_KEY= diff --git a/docker/nginx/conf.d/default.conf.template b/docker/nginx/conf.d/default.conf.template index a458412d1e..48d7da8cf5 100644 --- a/docker/nginx/conf.d/default.conf.template +++ b/docker/nginx/conf.d/default.conf.template @@ -39,7 +39,10 @@ server { proxy_pass http://web:3000; include proxy.conf; } - + location /mcp { + proxy_pass http://api:5001; + include proxy.conf; + } # placeholder for acme challenge location ${ACME_CHALLENGE_LOCATION} diff --git a/images/GitHub_README_if.png b/images/GitHub_README_if.png index 2a4e67264e..10c9d87b08 100644 Binary files a/images/GitHub_README_if.png and b/images/GitHub_README_if.png differ diff --git a/tests/unit_tests/events/test_provider_update_deadlock_prevention.py b/tests/unit_tests/events/test_provider_update_deadlock_prevention.py new file mode 100644 index 0000000000..47c175acd7 --- /dev/null +++ b/tests/unit_tests/events/test_provider_update_deadlock_prevention.py @@ -0,0 +1,248 @@ +import threading +from unittest.mock import Mock, patch + +from core.app.entities.app_invoke_entities import ChatAppGenerateEntity +from core.entities.provider_entities import QuotaUnit +from events.event_handlers.update_provider_when_message_created import ( + handle, + get_update_stats, +) +from models.provider import ProviderType +from sqlalchemy.exc import OperationalError + + +class TestProviderUpdateDeadlockPrevention: + """Test suite for deadlock prevention in Provider updates.""" + + def setup_method(self): + """Setup test fixtures.""" + self.mock_message = Mock() + self.mock_message.answer_tokens = 100 + + self.mock_app_config = Mock() + self.mock_app_config.tenant_id = "test-tenant-123" + + self.mock_model_conf = Mock() + self.mock_model_conf.provider = "openai" + + self.mock_system_config = Mock() + self.mock_system_config.current_quota_type = QuotaUnit.TOKENS + + self.mock_provider_config = Mock() + self.mock_provider_config.using_provider_type = ProviderType.SYSTEM + self.mock_provider_config.system_configuration = self.mock_system_config + + self.mock_provider_bundle = Mock() + self.mock_provider_bundle.configuration = self.mock_provider_config + + self.mock_model_conf.provider_model_bundle = self.mock_provider_bundle + + self.mock_generate_entity = Mock(spec=ChatAppGenerateEntity) + self.mock_generate_entity.app_config = self.mock_app_config + self.mock_generate_entity.model_conf = self.mock_model_conf + + @patch("events.event_handlers.update_provider_when_message_created.db") + def test_consolidated_handler_basic_functionality(self, mock_db): + """Test that the consolidated handler performs both updates correctly.""" + # Setup mock query chain + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 # 1 row affected + + # Call the handler + handle(self.mock_message, application_generate_entity=self.mock_generate_entity) + + # Verify db.session.query was called + assert mock_db.session.query.called + + # Verify commit was called + mock_db.session.commit.assert_called_once() + + # Verify no rollback was called + assert not mock_db.session.rollback.called + + @patch("events.event_handlers.update_provider_when_message_created.db") + def test_deadlock_retry_mechanism(self, mock_db): + """Test that deadlock errors trigger retry logic.""" + # Setup mock to raise deadlock error on first attempt, succeed on second + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + # First call raises deadlock, second succeeds + mock_db.session.commit.side_effect = [ + OperationalError("deadlock detected", None, None), + None, # Success on retry + ] + + # Call the handler + handle(self.mock_message, application_generate_entity=self.mock_generate_entity) + + # Verify commit was called twice (original + retry) + assert mock_db.session.commit.call_count == 2 + + # Verify rollback was called once (after first failure) + mock_db.session.rollback.assert_called_once() + + @patch("events.event_handlers.update_provider_when_message_created.db") + @patch("events.event_handlers.update_provider_when_message_created.time.sleep") + def test_exponential_backoff_timing(self, mock_sleep, mock_db): + """Test that retry delays follow exponential backoff pattern.""" + # Setup mock to fail twice, succeed on third attempt + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + mock_db.session.commit.side_effect = [ + OperationalError("deadlock detected", None, None), + OperationalError("deadlock detected", None, None), + None, # Success on third attempt + ] + + # Call the handler + handle(self.mock_message, application_generate_entity=self.mock_generate_entity) + + # Verify sleep was called twice with increasing delays + assert mock_sleep.call_count == 2 + + # First delay should be around 0.1s + jitter + first_delay = mock_sleep.call_args_list[0][0][0] + assert 0.1 <= first_delay <= 0.3 + + # Second delay should be around 0.2s + jitter + second_delay = mock_sleep.call_args_list[1][0][0] + assert 0.2 <= second_delay <= 0.4 + + def test_concurrent_handler_execution(self): + """Test that multiple handlers can run concurrently without deadlock.""" + results = [] + errors = [] + + def run_handler(): + try: + with patch( + "events.event_handlers.update_provider_when_message_created.db" + ) as mock_db: + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + handle( + self.mock_message, + application_generate_entity=self.mock_generate_entity, + ) + results.append("success") + except Exception as e: + errors.append(str(e)) + + # Run multiple handlers concurrently + threads = [] + for _ in range(5): + thread = threading.Thread(target=run_handler) + threads.append(thread) + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join(timeout=5) + + # Verify all handlers completed successfully + assert len(results) == 5 + assert len(errors) == 0 + + def test_performance_stats_tracking(self): + """Test that performance statistics are tracked correctly.""" + # Reset stats + stats = get_update_stats() + initial_total = stats["total_updates"] + + with patch( + "events.event_handlers.update_provider_when_message_created.db" + ) as mock_db: + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + # Call handler + handle( + self.mock_message, application_generate_entity=self.mock_generate_entity + ) + + # Check that stats were updated + updated_stats = get_update_stats() + assert updated_stats["total_updates"] == initial_total + 1 + assert updated_stats["successful_updates"] >= initial_total + 1 + + def test_non_chat_entity_ignored(self): + """Test that non-chat entities are ignored by the handler.""" + # Create a non-chat entity + mock_non_chat_entity = Mock() + mock_non_chat_entity.__class__.__name__ = "NonChatEntity" + + with patch( + "events.event_handlers.update_provider_when_message_created.db" + ) as mock_db: + # Call handler with non-chat entity + handle(self.mock_message, application_generate_entity=mock_non_chat_entity) + + # Verify no database operations were performed + assert not mock_db.session.query.called + assert not mock_db.session.commit.called + + @patch("events.event_handlers.update_provider_when_message_created.db") + def test_quota_calculation_tokens(self, mock_db): + """Test quota calculation for token-based quotas.""" + # Setup token-based quota + self.mock_system_config.current_quota_type = QuotaUnit.TOKENS + self.mock_message.answer_tokens = 150 + + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + # Call handler + handle(self.mock_message, application_generate_entity=self.mock_generate_entity) + + # Verify update was called with token count + update_calls = mock_query.update.call_args_list + + # Should have at least one call with quota_used update + quota_update_found = False + for call in update_calls: + values = call[0][0] # First argument to update() + if "quota_used" in values: + quota_update_found = True + break + + assert quota_update_found + + @patch("events.event_handlers.update_provider_when_message_created.db") + def test_quota_calculation_times(self, mock_db): + """Test quota calculation for times-based quotas.""" + # Setup times-based quota + self.mock_system_config.current_quota_type = QuotaUnit.TIMES + + mock_query = Mock() + mock_db.session.query.return_value = mock_query + mock_query.filter.return_value = mock_query + mock_query.order_by.return_value = mock_query + mock_query.update.return_value = 1 + + # Call handler + handle(self.mock_message, application_generate_entity=self.mock_generate_entity) + + # Verify update was called + assert mock_query.update.called + assert mock_db.session.commit.called diff --git a/web/.env.example b/web/.env.example index 78b4f33e8c..37bfc939eb 100644 --- a/web/.env.example +++ b/web/.env.example @@ -32,6 +32,9 @@ NEXT_PUBLIC_CSP_WHITELIST= # Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking NEXT_PUBLIC_ALLOW_EMBED= +# Allow rendering unsafe URLs which have "data:" scheme. +NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false + # Github Access Token, used for invoking Github API NEXT_PUBLIC_GITHUB_ACCESS_TOKEN= # The maximum number of top-k value for RAG. @@ -56,3 +59,5 @@ NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=true NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=true NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=true +# The maximum number of tree node depth for workflow +NEXT_PUBLIC_MAX_TREE_DEPTH=50 diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx index 084adceef2..3d572b926a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/appCard' import Loading from '@/app/components/base/loading' +import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' import { ToastContext } from '@/app/components/base/toast' import { fetchAppDetail, @@ -31,6 +32,8 @@ const CardView: FC = ({ appId, isInPanel, className }) => { const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) + const showMCPCard = isInPanel + const updateAppDetail = async () => { try { const res = await fetchAppDetail({ url: '/apps', id: appId }) @@ -117,6 +120,11 @@ const CardView: FC = ({ appId, isInPanel, className }) => { isInPanel={isInPanel} onChangeStatus={onChangeApiStatus} /> + {showMCPCard && ( + + )} ) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index 4afba06eae..646c8bd93d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -18,9 +18,10 @@ const queryDateFormat = 'YYYY-MM-DD HH:mm' export type IChartViewProps = { appId: string + headerRight: React.ReactNode } -export default function ChartView({ appId }: IChartViewProps) { +export default function ChartView({ appId, headerRight }: IChartViewProps) { const { t } = useTranslation() const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' @@ -46,19 +47,24 @@ export default function ChartView({ appId }: IChartViewProps) { return (
-
- {t('appOverview.analysis.title')} - ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} - className='mt-0 !w-40' - onSelect={(item) => { - const id = item.value - const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' - const name = item.name || t('appLog.filter.period.allTime') - onSelect({ value, name }) - }} - defaultValue={'2'} - /> +
+
{t('common.appMenus.overview')}
+
+
+ ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} + className='mt-0 !w-40' + onSelect={(item) => { + const id = item.value + const value = TIME_PERIOD_MAPPING[id]?.value ?? '-1' + const name = item.name || t('appLog.filter.period.allTime') + onSelect({ value, name }) + }} + defaultValue={'2'} + /> +
+ {headerRight} +
{!isWorkflow && (
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx index 0f1bb7e18d..e0c09e739e 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx @@ -1,6 +1,5 @@ import React from 'react' import ChartView from './chartView' -import CardView from './cardView' import TracingPanel from './tracing/panel' import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel' @@ -16,11 +15,12 @@ const Overview = async (props: IDevelopProps) => { } = params return ( -
+
- - - + } + />
) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 0efc5082a4..2afe451fe1 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import TracingIcon from './tracing-icon' import ProviderPanel from './provider-panel' -import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type' +import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import { TracingProvider } from './type' import ProviderConfigModal from './provider-config-modal' import Indicator from '@/app/components/header/indicator' @@ -23,11 +23,14 @@ export type PopupProps = { onStatusChange: (enabled: boolean) => void chosenProvider: TracingProvider | null onChooseProvider: (provider: TracingProvider) => void + arizeConfig: ArizeConfig | null + phoenixConfig: PhoenixConfig | null langSmithConfig: LangSmithConfig | null langFuseConfig: LangFuseConfig | null opikConfig: OpikConfig | null weaveConfig: WeaveConfig | null - onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void + aliyunConfig: AliyunConfig | null + onConfigUpdated: (provider: TracingProvider, payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => void onConfigRemoved: (provider: TracingProvider) => void } @@ -38,10 +41,13 @@ const ConfigPopup: FC = ({ onStatusChange, chosenProvider, onChooseProvider, + arizeConfig, + phoenixConfig, langSmithConfig, langFuseConfig, opikConfig, weaveConfig, + aliyunConfig, onConfigUpdated, onConfigRemoved, }) => { @@ -65,7 +71,7 @@ const ConfigPopup: FC = ({ } }, [onChooseProvider]) - const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => { + const handleConfigUpdated = useCallback((payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => { onConfigUpdated(currentProvider!, payload) hideConfigModal() }, [currentProvider, hideConfigModal, onConfigUpdated]) @@ -75,8 +81,8 @@ const ConfigPopup: FC = ({ hideConfigModal() }, [currentProvider, hideConfigModal, onConfigRemoved]) - const providerAllConfigured = langSmithConfig && langFuseConfig && opikConfig && weaveConfig - const providerAllNotConfigured = !langSmithConfig && !langFuseConfig && !opikConfig && !weaveConfig + const providerAllConfigured = arizeConfig && phoenixConfig && langSmithConfig && langFuseConfig && opikConfig && weaveConfig && aliyunConfig + const providerAllNotConfigured = !arizeConfig && !phoenixConfig && !langSmithConfig && !langFuseConfig && !opikConfig && !weaveConfig && !aliyunConfig const switchContent = ( = ({ disabled={providerAllNotConfigured} /> ) + const arizePanel = ( + + ) + + const phoenixPanel = ( + + ) + const langSmithPanel = ( = ({ key="weave-provider-panel" /> ) + + const aliyunPanel = ( + + ) const configuredProviderPanel = () => { const configuredPanels: JSX.Element[] = [] @@ -152,12 +197,27 @@ const ConfigPopup: FC = ({ if (weaveConfig) configuredPanels.push(weavePanel) + if (arizeConfig) + configuredPanels.push(arizePanel) + + if (phoenixConfig) + configuredPanels.push(phoenixPanel) + + if (aliyunConfig) + configuredPanels.push(aliyunPanel) + return configuredPanels } const moreProviderPanel = () => { const notConfiguredPanels: JSX.Element[] = [] + if (!arizeConfig) + notConfiguredPanels.push(arizePanel) + + if (!phoenixConfig) + notConfiguredPanels.push(phoenixPanel) + if (!langFuseConfig) notConfiguredPanels.push(langfusePanel) @@ -170,16 +230,25 @@ const ConfigPopup: FC = ({ if (!weaveConfig) notConfiguredPanels.push(weavePanel) + if (!aliyunConfig) + notConfiguredPanels.push(aliyunPanel) + return notConfiguredPanels } const configuredProviderConfig = () => { + if (currentProvider === TracingProvider.arize) + return arizeConfig + if (currentProvider === TracingProvider.phoenix) + return phoenixConfig if (currentProvider === TracingProvider.langSmith) return langSmithConfig if (currentProvider === TracingProvider.langfuse) return langFuseConfig if (currentProvider === TracingProvider.opik) return opikConfig + if (currentProvider === TracingProvider.aliyun) + return aliyunConfig return weaveConfig } @@ -220,22 +289,25 @@ const ConfigPopup: FC = ({ ? ( <>
{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}
-
+
{langfusePanel} {langSmithPanel} {opikPanel} {weavePanel} + {arizePanel} + {phoenixPanel} + {aliyunPanel}
) : ( <>
{t(`${I18N_PREFIX}.configProviderTitle.configured`)}
-
+
{configuredProviderPanel()}
{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}
-
+
{moreProviderPanel()}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts index 5d3c4076bd..4c81b63ea2 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts @@ -1,8 +1,11 @@ import { TracingProvider } from './type' export const docURL = { + [TracingProvider.arize]: 'https://docs.arize.com/arize', + [TracingProvider.phoenix]: 'https://docs.arize.com/phoenix', [TracingProvider.langSmith]: 'https://docs.smith.langchain.com/', [TracingProvider.langfuse]: 'https://docs.langfuse.com', [TracingProvider.opik]: 'https://www.comet.com/docs/opik/tracing/integrations/dify#setup-instructions', [TracingProvider.weave]: 'https://weave-docs.wandb.ai/', + [TracingProvider.aliyun]: 'https://help.aliyun.com/zh/arms/tracing-analysis/untitled-document-1750672984680', } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 8575117c41..8bf18904be 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -7,12 +7,12 @@ import { import { useTranslation } from 'react-i18next' import { usePathname } from 'next/navigation' import { useBoolean } from 'ahooks' -import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type' +import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import { TracingProvider } from './type' import TracingIcon from './tracing-icon' import ConfigButton from './config-button' import cn from '@/utils/classnames' -import { LangfuseIcon, LangsmithIcon, OpikIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' +import { AliyunIcon, ArizeIcon, LangfuseIcon, LangsmithIcon, OpikIcon, PhoenixIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import Indicator from '@/app/components/header/indicator' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' import type { TracingStatus } from '@/models/app' @@ -23,19 +23,6 @@ import Divider from '@/app/components/base/divider' const I18N_PREFIX = 'app.tracing' -const Title = ({ - className, -}: { - className?: string -}) => { - const { t } = useTranslation() - - return ( -
- {t('common.appMenus.overview')} -
- ) -} const Panel: FC = () => { const { t } = useTranslation() const pathname = usePathname() @@ -75,24 +62,33 @@ const Panel: FC = () => { } const inUseTracingProvider: TracingProvider | null = tracingStatus?.tracing_provider || null - const InUseProviderIcon - = inUseTracingProvider === TracingProvider.langSmith - ? LangsmithIcon - : inUseTracingProvider === TracingProvider.langfuse - ? LangfuseIcon - : inUseTracingProvider === TracingProvider.opik - ? OpikIcon - : inUseTracingProvider === TracingProvider.weave - ? WeaveIcon - : LangsmithIcon + const providerIconMap: Record> = { + [TracingProvider.arize]: ArizeIcon, + [TracingProvider.phoenix]: PhoenixIcon, + [TracingProvider.langSmith]: LangsmithIcon, + [TracingProvider.langfuse]: LangfuseIcon, + [TracingProvider.opik]: OpikIcon, + [TracingProvider.weave]: WeaveIcon, + [TracingProvider.aliyun]: AliyunIcon, + } + const InUseProviderIcon = inUseTracingProvider ? providerIconMap[inUseTracingProvider] : undefined + const [arizeConfig, setArizeConfig] = useState(null) + const [phoenixConfig, setPhoenixConfig] = useState(null) const [langSmithConfig, setLangSmithConfig] = useState(null) const [langFuseConfig, setLangFuseConfig] = useState(null) const [opikConfig, setOpikConfig] = useState(null) const [weaveConfig, setWeaveConfig] = useState(null) - const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig) + const [aliyunConfig, setAliyunConfig] = useState(null) + const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig || opikConfig || weaveConfig || arizeConfig || phoenixConfig || aliyunConfig) const fetchTracingConfig = async () => { + const { tracing_config: arizeConfig, has_not_configured: arizeHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.arize }) + if (!arizeHasNotConfig) + setArizeConfig(arizeConfig as ArizeConfig) + const { tracing_config: phoenixConfig, has_not_configured: phoenixHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.phoenix }) + if (!phoenixHasNotConfig) + setPhoenixConfig(phoenixConfig as PhoenixConfig) const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith }) if (!langSmithHasNotConfig) setLangSmithConfig(langSmithConfig as LangSmithConfig) @@ -105,12 +101,19 @@ const Panel: FC = () => { const { tracing_config: weaveConfig, has_not_configured: weaveHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.weave }) if (!weaveHasNotConfig) setWeaveConfig(weaveConfig as WeaveConfig) + const { tracing_config: aliyunConfig, has_not_configured: aliyunHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.aliyun }) + if (!aliyunHasNotConfig) + setAliyunConfig(aliyunConfig as AliyunConfig) } const handleTracingConfigUpdated = async (provider: TracingProvider) => { // call api to hide secret key value const { tracing_config } = await doFetchTracingConfig({ appId, provider }) - if (provider === TracingProvider.langSmith) + if (provider === TracingProvider.arize) + setArizeConfig(tracing_config as ArizeConfig) + else if (provider === TracingProvider.phoenix) + setPhoenixConfig(tracing_config as PhoenixConfig) + else if (provider === TracingProvider.langSmith) setLangSmithConfig(tracing_config as LangSmithConfig) else if (provider === TracingProvider.langfuse) setLangFuseConfig(tracing_config as LangFuseConfig) @@ -118,10 +121,16 @@ const Panel: FC = () => { setOpikConfig(tracing_config as OpikConfig) else if (provider === TracingProvider.weave) setWeaveConfig(tracing_config as WeaveConfig) + else if (provider === TracingProvider.aliyun) + setAliyunConfig(tracing_config as AliyunConfig) } const handleTracingConfigRemoved = (provider: TracingProvider) => { - if (provider === TracingProvider.langSmith) + if (provider === TracingProvider.arize) + setArizeConfig(null) + else if (provider === TracingProvider.phoenix) + setPhoenixConfig(null) + else if (provider === TracingProvider.langSmith) setLangSmithConfig(null) else if (provider === TracingProvider.langfuse) setLangFuseConfig(null) @@ -129,6 +138,8 @@ const Panel: FC = () => { setOpikConfig(null) else if (provider === TracingProvider.weave) setWeaveConfig(null) + else if (provider === TracingProvider.aliyun) + setAliyunConfig(null) if (provider === inUseTracingProvider) { handleTracingStatusChange({ enabled: false, @@ -154,7 +165,6 @@ const Panel: FC = () => { if (!isLoaded) { return (
- <div className='w-[200px]'> <Loading /> </div> @@ -163,8 +173,7 @@ const Panel: FC = () => { } return ( - <div className={cn('mb-3 flex items-center justify-between')}> - <Title className='h-[41px]' /> + <div className={cn('flex items-center justify-between')}> <div className={cn( 'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter', @@ -185,10 +194,13 @@ const Panel: FC = () => { onStatusChange={handleTracingEnabledChange} chosenProvider={inUseTracingProvider} onChooseProvider={handleChooseProvider} + arizeConfig={arizeConfig} + phoenixConfig={phoenixConfig} langSmithConfig={langSmithConfig} langFuseConfig={langFuseConfig} opikConfig={opikConfig} weaveConfig={weaveConfig} + aliyunConfig={aliyunConfig} onConfigUpdated={handleTracingConfigUpdated} onConfigRemoved={handleTracingConfigRemoved} controlShowPopup={controlShowPopup} @@ -220,10 +232,13 @@ const Panel: FC = () => { onStatusChange={handleTracingEnabledChange} chosenProvider={inUseTracingProvider} onChooseProvider={handleChooseProvider} + arizeConfig={arizeConfig} + phoenixConfig={phoenixConfig} langSmithConfig={langSmithConfig} langFuseConfig={langFuseConfig} opikConfig={opikConfig} weaveConfig={weaveConfig} + aliyunConfig={aliyunConfig} onConfigUpdated={handleTracingConfigUpdated} onConfigRemoved={handleTracingConfigRemoved} controlShowPopup={controlShowPopup} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index c0b52a9b10..318f1f61d6 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import Field from './field' -import type { LangFuseConfig, LangSmithConfig, OpikConfig, WeaveConfig } from './type' +import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, WeaveConfig } from './type' import { TracingProvider } from './type' import { docURL } from './config' import { @@ -22,15 +22,28 @@ import Divider from '@/app/components/base/divider' type Props = { appId: string type: TracingProvider - payload?: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | null + payload?: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig | null onRemoved: () => void onCancel: () => void - onSaved: (payload: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig) => void + onSaved: (payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig) => void onChosen: (provider: TracingProvider) => void } const I18N_PREFIX = 'app.tracing.configProvider' +const arizeConfigTemplate = { + api_key: '', + space_id: '', + project: '', + endpoint: '', +} + +const phoenixConfigTemplate = { + api_key: '', + project: '', + endpoint: '', +} + const langSmithConfigTemplate = { api_key: '', project: '', @@ -55,6 +68,13 @@ const weaveConfigTemplate = { entity: '', project: '', endpoint: '', + host: '', +} + +const aliyunConfigTemplate = { + app_name: '', + license_key: '', + endpoint: '', } const ProviderConfigModal: FC<Props> = ({ @@ -70,11 +90,17 @@ const ProviderConfigModal: FC<Props> = ({ const isEdit = !!payload const isAdd = !isEdit const [isSaving, setIsSaving] = useState(false) - const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig>((() => { + const [config, setConfig] = useState<ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig>((() => { if (isEdit) return payload - if (type === TracingProvider.langSmith) + if (type === TracingProvider.arize) + return arizeConfigTemplate + + else if (type === TracingProvider.phoenix) + return phoenixConfigTemplate + + else if (type === TracingProvider.langSmith) return langSmithConfigTemplate else if (type === TracingProvider.langfuse) @@ -83,6 +109,9 @@ const ProviderConfigModal: FC<Props> = ({ else if (type === TracingProvider.opik) return opikConfigTemplate + else if (type === TracingProvider.aliyun) + return aliyunConfigTemplate + return weaveConfigTemplate })()) const [isShowRemoveConfirm, { @@ -114,6 +143,24 @@ const ProviderConfigModal: FC<Props> = ({ const checkValid = useCallback(() => { let errorMessage = '' + if (type === TracingProvider.arize) { + const postData = config as ArizeConfig + if (!postData.api_key) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' }) + if (!postData.space_id) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'Space ID' }) + if (!errorMessage && !postData.project) + errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) }) + } + + if (type === TracingProvider.phoenix) { + const postData = config as PhoenixConfig + if (!postData.api_key) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' }) + if (!errorMessage && !postData.project) + errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) }) + } + if (type === TracingProvider.langSmith) { const postData = config as LangSmithConfig if (!postData.api_key) @@ -145,6 +192,16 @@ const ProviderConfigModal: FC<Props> = ({ errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) }) } + if (type === TracingProvider.aliyun) { + const postData = config as AliyunConfig + if (!errorMessage && !postData.app_name) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'App Name' }) + if (!errorMessage && !postData.license_key) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'License Key' }) + if (!errorMessage && !postData.endpoint) + errorMessage = t('common.errorMsg.fieldRequired', { field: 'Endpoint' }) + } + return errorMessage }, [config, t, type]) const handleSave = useCallback(async () => { @@ -194,6 +251,93 @@ const ProviderConfigModal: FC<Props> = ({ </div> <div className='space-y-4'> + {type === TracingProvider.arize && ( + <> + <Field + label='API Key' + labelClassName='!text-sm' + isRequired + value={(config as ArizeConfig).api_key} + onChange={handleConfigChange('api_key')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!} + /> + <Field + label='Space ID' + labelClassName='!text-sm' + isRequired + value={(config as ArizeConfig).space_id} + onChange={handleConfigChange('space_id')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'Space ID' })!} + /> + <Field + label={t(`${I18N_PREFIX}.project`)!} + labelClassName='!text-sm' + isRequired + value={(config as ArizeConfig).project} + onChange={handleConfigChange('project')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} + /> + <Field + label='Endpoint' + labelClassName='!text-sm' + value={(config as ArizeConfig).endpoint} + onChange={handleConfigChange('endpoint')} + placeholder={'https://otlp.arize.com'} + /> + </> + )} + {type === TracingProvider.phoenix && ( + <> + <Field + label='API Key' + labelClassName='!text-sm' + isRequired + value={(config as PhoenixConfig).api_key} + onChange={handleConfigChange('api_key')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!} + /> + <Field + label={t(`${I18N_PREFIX}.project`)!} + labelClassName='!text-sm' + isRequired + value={(config as PhoenixConfig).project} + onChange={handleConfigChange('project')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!} + /> + <Field + label='Endpoint' + labelClassName='!text-sm' + value={(config as PhoenixConfig).endpoint} + onChange={handleConfigChange('endpoint')} + placeholder={'https://app.phoenix.arize.com'} + /> + </> + )} + {type === TracingProvider.aliyun && ( + <> + <Field + label='License Key' + labelClassName='!text-sm' + isRequired + value={(config as AliyunConfig).license_key} + onChange={handleConfigChange('license_key')} + placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'License Key' })!} + /> + <Field + label='Endpoint' + labelClassName='!text-sm' + value={(config as AliyunConfig).endpoint} + onChange={handleConfigChange('endpoint')} + placeholder={'https://tracing.arms.aliyuncs.com'} + /> + <Field + label='App Name' + labelClassName='!text-sm' + value={(config as AliyunConfig).app_name} + onChange={handleConfigChange('app_name')} + /> + </> + )} {type === TracingProvider.weave && ( <> <Field @@ -226,6 +370,13 @@ const ProviderConfigModal: FC<Props> = ({ onChange={handleConfigChange('endpoint')} placeholder={'https://trace.wandb.ai/'} /> + <Field + label='Host' + labelClassName='!text-sm' + value={(config as WeaveConfig).host} + onChange={handleConfigChange('host')} + placeholder={'https://api.wandb.ai'} + /> </> )} {type === TracingProvider.langSmith && ( diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx index bdccc502d6..4a45e0cab7 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx @@ -7,7 +7,7 @@ import { import { useTranslation } from 'react-i18next' import { TracingProvider } from './type' import cn from '@/utils/classnames' -import { LangfuseIconBig, LangsmithIconBig, OpikIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing' +import { AliyunIconBig, ArizeIconBig, LangfuseIconBig, LangsmithIconBig, OpikIconBig, PhoenixIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing' import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' const I18N_PREFIX = 'app.tracing' @@ -24,10 +24,13 @@ type Props = { const getIcon = (type: TracingProvider) => { return ({ + [TracingProvider.arize]: ArizeIconBig, + [TracingProvider.phoenix]: PhoenixIconBig, [TracingProvider.langSmith]: LangsmithIconBig, [TracingProvider.langfuse]: LangfuseIconBig, [TracingProvider.opik]: OpikIconBig, [TracingProvider.weave]: WeaveIconBig, + [TracingProvider.aliyun]: AliyunIconBig, })[type] } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts index 386c58974e..78bca41ad2 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts @@ -1,8 +1,24 @@ export enum TracingProvider { + arize = 'arize', + phoenix = 'phoenix', langSmith = 'langsmith', langfuse = 'langfuse', opik = 'opik', weave = 'weave', + aliyun = 'aliyun', +} + +export type ArizeConfig = { + api_key: string + space_id: string + project: string + endpoint: string +} + +export type PhoenixConfig = { + api_key: string + project: string + endpoint: string } export type LangSmithConfig = { @@ -29,4 +45,11 @@ export type WeaveConfig = { entity: string project: string endpoint: string + host: string +} + +export type AliyunConfig = { + app_name: string + license_key: string + endpoint: string } diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 31b9ed87c2..f50cc10520 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -36,6 +36,7 @@ import AccessControl from '@/app/components/app/app-access-control' import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import { formatTime } from '@/utils/time' +import { useGetUserCanAccessApp } from '@/service/access-control' export type AppCardProps = { app: App @@ -190,6 +191,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { }, [onRefresh, mutateApps, setShowAccessControl]) const Operations = (props: HtmlContentProps) => { + const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: app?.id, enabled: (!!props?.open && systemFeatures.webapp_auth.enabled) }) const onMouseLeave = async () => { props.onClose?.() } @@ -267,10 +269,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { </button> </> )} - <Divider className="my-1" /> - <button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}> - <span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span> - </button> + { + (isGettingUserCanAccessApp || !userCanAccessApp?.result) ? null : <> + <Divider className="my-1" /> + <button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}> + <span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span> + </button> + </> + } <Divider className="my-1" /> { systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <> @@ -333,7 +339,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { <div className='flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary'> <div className='truncate' title={app.author_name}>{app.author_name}</div> <div>·</div> - <div className='truncate'>{EditTimeText}</div> + <div className='truncate' title={EditTimeText}>{EditTimeText}</div> </div> </div> <div className='flex h-5 w-5 shrink-0 items-center justify-center'> diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 1b7ff39383..2aa192fb02 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import { RiApps2Line, + RiDragDropLine, RiExchange2Line, RiFile4Line, RiMessage3Line, @@ -16,7 +17,8 @@ import { } from '@remixicon/react' import AppCard from './AppCard' import NewAppCard from './NewAppCard' -import useAppsQueryState from './hooks/useAppsQueryState' +import useAppsQueryState from './hooks/use-apps-query-state' +import { useDSLDragDrop } from './hooks/use-dsl-drag-drop' import type { AppListResponse } from '@/models/app' import { fetchAppList } from '@/service/apps' import { useAppContext } from '@/context/app-context' @@ -29,6 +31,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st import TagManagementModal from '@/app/components/base/tag-management' import TagFilter from '@/app/components/base/tag-management/filter' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' +import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal' const getKey = ( pageIndex: number, @@ -67,6 +70,9 @@ const Apps = () => { const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs) const [searchKeywords, setSearchKeywords] = useState(keywords) const newAppCardRef = useRef<HTMLDivElement>(null) + const containerRef = useRef<HTMLDivElement>(null) + const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) + const [droppedDSLFile, setDroppedDSLFile] = useState<File | undefined>() const setKeywords = useCallback((keywords: string) => { setQuery(prev => ({ ...prev, keywords })) }, [setQuery]) @@ -74,6 +80,17 @@ const Apps = () => { setQuery(prev => ({ ...prev, tagIDs })) }, [setQuery]) + const handleDSLFileDropped = useCallback((file: File) => { + setDroppedDSLFile(file) + setShowCreateFromDSLModal(true) + }, []) + + const { dragging } = useDSLDragDrop({ + onDSLFileDropped: handleDSLFileDropped, + containerRef, + enabled: isCurrentWorkspaceEditor, + }) + const { data, isLoading, error, setSize, mutate } = useSWRInfinite( (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords), fetchAppList, @@ -88,11 +105,11 @@ const Apps = () => { const anchorRef = useRef<HTMLDivElement>(null) const options = [ { value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> }, + { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> }, + { value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> }, { value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> }, - { value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> }, - { value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> }, ] useEffect(() => { @@ -151,47 +168,81 @@ const Apps = () => { return ( <> - <div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'> - <TabSliderNew - value={activeTab} - onChange={setActiveTab} - options={options} - /> - <div className='flex items-center gap-2'> - <CheckboxWithLabel - className='mr-2' - label={t('app.showMyCreatedAppsOnly')} - isChecked={isCreatedByMe} - onChange={handleCreatedByMeChange} - /> - <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} /> - <Input - showLeftIcon - showClearIcon - wrapperClassName='w-[200px]' - value={keywords} - onChange={e => handleKeywordsChange(e.target.value)} - onClear={() => handleKeywordsChange('')} + <div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'> + {dragging && ( + <div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2"> + </div> + )} + + <div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'> + <TabSliderNew + value={activeTab} + onChange={setActiveTab} + options={options} /> + <div className='flex items-center gap-2'> + <CheckboxWithLabel + className='mr-2' + label={t('app.showMyCreatedAppsOnly')} + isChecked={isCreatedByMe} + onChange={handleCreatedByMeChange} + /> + <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} /> + <Input + showLeftIcon + showClearIcon + wrapperClassName='w-[200px]' + value={keywords} + onChange={e => handleKeywordsChange(e.target.value)} + onClear={() => handleKeywordsChange('')} + /> + </div> </div> + {(data && data[0].total > 0) + ? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> + {isCurrentWorkspaceEditor + && <NewAppCard ref={newAppCardRef} onSuccess={mutate} />} + {data.map(({ data: apps }) => apps.map(app => ( + <AppCard key={app.id} app={app} onRefresh={mutate} /> + )))} + </div> + : <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> + {isCurrentWorkspaceEditor + && <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />} + <NoAppsFound /> + </div>} + + {isCurrentWorkspaceEditor && ( + <div + className={`flex items-center justify-center gap-2 py-4 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`} + role="region" + aria-label={t('app.newApp.dropDSLToCreateApp')} + > + <RiDragDropLine className="h-4 w-4" /> + <span className="system-xs-regular">{t('app.newApp.dropDSLToCreateApp')}</span> + </div> + )} + <CheckModal /> + <div ref={anchorRef} className='h-0'> </div> + {showTagManagementModal && ( + <TagManagementModal type='app' show={showTagManagementModal} /> + )} </div> - {(data && data[0].total > 0) - ? <div className='relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> - {isCurrentWorkspaceEditor - && <NewAppCard ref={newAppCardRef} onSuccess={mutate} />} - {data.map(({ data: apps }) => apps.map(app => ( - <AppCard key={app.id} app={app} onRefresh={mutate} /> - )))} - </div> - : <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'> - {isCurrentWorkspaceEditor - && <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />} - <NoAppsFound /> - </div>} - <CheckModal /> - <div ref={anchorRef} className='h-0'> </div> - {showTagManagementModal && ( - <TagManagementModal type='app' show={showTagManagementModal} /> + + {showCreateFromDSLModal && ( + <CreateFromDSLModal + show={showCreateFromDSLModal} + onClose={() => { + setShowCreateFromDSLModal(false) + setDroppedDSLFile(undefined) + }} + onSuccess={() => { + setShowCreateFromDSLModal(false) + setDroppedDSLFile(undefined) + mutate() + }} + droppedFile={droppedDSLFile} + /> )} </> ) diff --git a/web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts b/web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts similarity index 100% rename from web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts rename to web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts diff --git a/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts b/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts new file mode 100644 index 0000000000..96942ec54e --- /dev/null +++ b/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react' + +type DSLDragDropHookProps = { + onDSLFileDropped: (file: File) => void + containerRef: React.RefObject<HTMLDivElement> + enabled?: boolean +} + +export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true }: DSLDragDropHookProps) => { + const [dragging, setDragging] = useState(false) + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.dataTransfer?.types.includes('Files')) + setDragging(true) + } + + const handleDragOver = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + } + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.relatedTarget === null || !containerRef.current?.contains(e.relatedTarget as Node)) + setDragging(false) + } + + const handleDrop = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragging(false) + + if (!e.dataTransfer) + return + + const files = [...e.dataTransfer.files] + if (files.length === 0) + return + + const file = files[0] + if (file.name.toLowerCase().endsWith('.yaml') || file.name.toLowerCase().endsWith('.yml')) + onDSLFileDropped(file) + } + + useEffect(() => { + if (!enabled) + return + + const current = containerRef.current + if (current) { + current.addEventListener('dragenter', handleDragEnter) + current.addEventListener('dragover', handleDragOver) + current.addEventListener('dragleave', handleDragLeave) + current.addEventListener('drop', handleDrop) + } + return () => { + if (current) { + current.removeEventListener('dragenter', handleDragEnter) + current.removeEventListener('dragover', handleDragOver) + current.removeEventListener('dragleave', handleDragLeave) + current.removeEventListener('drop', handleDrop) + } + } + }, [containerRef, enabled]) + + return { + dragging: enabled ? dragging : false, + } +} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 94cd5ad562..426778c835 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -25,9 +25,8 @@ import Loading from '@/app/components/base/loading' import DatasetDetailContext from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { LanguagesSupported } from '@/i18n/language' import { useStore } from '@/app/components/app/store' -import { getLocaleOnClient } from '@/i18n' +import { useDocLink } from '@/context/i18n' import { useAppContext } from '@/context/app-context' import Tooltip from '@/app/components/base/tooltip' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' @@ -45,9 +44,9 @@ type IExtraInfoProps = { } const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { - const locale = getLocaleOnClient() const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile) const { t } = useTranslation() + const docLink = useDocLink() const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const relatedAppsTotal = relatedApps?.data?.length || 0 @@ -63,7 +62,6 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { <Tooltip position='right' noDecoration - needsDelay popupContent={ <LinkedAppsPanel relatedApps={relatedApps.data} @@ -78,7 +76,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { </Tooltip> )} - {isMobile && <div className={classNames('uppercase text-xs text-text-tertiary font-medium pb-2 pt-4', 'flex items-center justify-center !px-0 gap-1')}> + {isMobile && <div className={classNames('pb-2 pt-4 text-xs font-medium uppercase text-text-tertiary', 'flex items-center justify-center gap-1 !px-0')}> {relatedAppsTotal || '--'} <PaperClipIcon className='h-4 w-4 text-text-secondary' /> </div>} @@ -88,7 +86,6 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { <Tooltip position='right' noDecoration - needsDelay popupContent={ <div className='w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-4'> <div className='inline-flex rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle p-2'> @@ -97,11 +94,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { <div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div> <a className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent' - href={ - locale === LanguagesSupported[1] - ? 'https://docs.dify.ai/zh-hans/guides/knowledge-base/integrate-knowledge-within-application' - : 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application' - } + href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')} target='_blank' rel='noopener noreferrer' > <RiBookOpenLine className='mr-1 text-text-accent' /> diff --git a/web/app/(commonLayout)/datasets/Datasets.tsx b/web/app/(commonLayout)/datasets/Datasets.tsx index 28461e8617..2d4848e92e 100644 --- a/web/app/(commonLayout)/datasets/Datasets.tsx +++ b/web/app/(commonLayout)/datasets/Datasets.tsx @@ -81,7 +81,7 @@ const Datasets = ({ currentContainer?.removeEventListener('scroll', onScroll) onScroll.cancel() } - }, [onScroll]) + }, [containerRef, onScroll]) return ( <nav className='grid shrink-0 grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'> diff --git a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx index 792d9904da..f3532f398d 100644 --- a/web/app/(commonLayout)/datasets/NewDatasetCard.tsx +++ b/web/app/(commonLayout)/datasets/NewDatasetCard.tsx @@ -5,34 +5,34 @@ import { RiAddLine, RiArrowRightLine, } from '@remixicon/react' +import Link from 'next/link' -const CreateAppCard = ( - { - ref, - ..._ - }, -) => { +type CreateAppCardProps = { + ref?: React.Ref<HTMLAnchorElement> +} + +const CreateAppCard = ({ ref }: CreateAppCardProps) => { const { t } = useTranslation() return ( <div className='bg-background-default-dimm flex min-h-[160px] flex-col rounded-xl border-[0.5px] border-components-panel-border transition-all duration-200 ease-in-out' > - <a ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}> + <Link ref={ref} className='group flex grow cursor-pointer items-start p-4' href={`${basePath}/datasets/create`}> <div className='flex items-center gap-3'> <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-regular bg-background-default-lighter p-2 group-hover:border-solid group-hover:border-effects-highlight group-hover:bg-background-default-dodge' > - <RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/> + <RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent' /> </div> <div className='system-md-semibold text-text-secondary group-hover:text-text-accent'>{t('dataset.createDataset')}</div> </div> - </a> + </Link> <div className='system-xs-regular p-4 pt-0 text-text-tertiary'>{t('dataset.createDatasetIntro')}</div> - <a className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}> + <Link className='group flex cursor-pointer items-center gap-1 rounded-b-xl border-t-[0.5px] border-divider-subtle p-4' href={`${basePath}/datasets/connect`}> <div className='system-xs-medium text-text-tertiary group-hover:text-text-accent'>{t('dataset.connectDataset')}</div> <RiArrowRightLine className='h-3.5 w-3.5 text-text-tertiary group-hover:text-text-accent' /> - </a> + </Link> </div> ) } diff --git a/web/app/(commonLayout)/datasets/layout.tsx b/web/app/(commonLayout)/datasets/layout.tsx index aecb537aa6..5f97d853ef 100644 --- a/web/app/(commonLayout)/datasets/layout.tsx +++ b/web/app/(commonLayout)/datasets/layout.tsx @@ -1,9 +1,25 @@ 'use client' +import Loading from '@/app/components/base/loading' +import { useAppContext } from '@/context/app-context' import { ExternalApiPanelProvider } from '@/context/external-api-panel-context' import { ExternalKnowledgeApiProvider } from '@/context/external-knowledge-api-context' +import { useRouter } from 'next/navigation' +import { useEffect } from 'react' export default function DatasetsLayout({ children }: { children: React.ReactNode }) { + const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, currentWorkspace, isLoadingCurrentWorkspace } = useAppContext() + const router = useRouter() + + useEffect(() => { + if (isLoadingCurrentWorkspace || !currentWorkspace.id) + return + if (!(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)) + router.replace('/apps') + }, [isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace, currentWorkspace, router]) + + if (isLoadingCurrentWorkspace || !(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)) + return <Loading type='app' /> return ( <ExternalKnowledgeApiProvider> <ExternalApiPanelProvider> diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx index 4d00b7b2b5..ebb2e6a806 100644 --- a/web/app/(commonLayout)/datasets/template/template.en.mdx +++ b/web/app/(commonLayout)/datasets/template/template.en.mdx @@ -54,7 +54,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi </Property> <Property name='indexing_technique' type='string' key='indexing_technique'> Index mode - - <code>high_quality</code> High quality: embedding using embedding model, built as vector database index + - <code>high_quality</code> High quality: Embedding using embedding model, built as vector database index - <code>economy</code> Economy: Build using inverted index of keyword table index </Property> <Property name='doc_form' type='string' key='doc_form'> @@ -1124,6 +1124,186 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi <hr className='ml-0 mr-0' /> +<Heading + url='/datasets/{dataset_id}/documents/{document_id}' + method='GET' + title='Get Document Detail' + name='#get-document-detail' +/> +<Row> + <Col> + Get a document's detail. + ### Path + - `dataset_id` (string) Dataset ID + - `document_id` (string) Document ID + + ### Query + - `metadata` (string) Metadata filter, can be `all`, `only`, or `without`. Default is `all`. + + ### Response + Returns the document's detail. + </Col> + <Col sticky> + ### Request Example + <CodeGroup title="Request" tag="GET" label="/datasets/{dataset_id}/documents/{document_id}" targetCode={`curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \\\n-H 'Authorization: Bearer {api_key}'`}> + ```bash {{ title: 'cURL' }} + curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \ + -H 'Authorization: Bearer {api_key}' + ``` + </CodeGroup> + + ### Response Example + <CodeGroup title="Response"> + ```json {{ title: 'Response' }} + { + "id": "f46ae30c-5c11-471b-96d0-464f5f32a7b2", + "position": 1, + "data_source_type": "upload_file", + "data_source_info": { + "upload_file": { + ... + } + }, + "dataset_process_rule_id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_process_rule": { + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "document_process_rule": { + "id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_id": "48a0db76-d1a9-46c1-ae35-2baaa919a8a9", + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "name": "xxxx", + "created_from": "web", + "created_by": "17f71940-a7b5-4c77-b60f-2bd645c1ffa0", + "created_at": 1750464191, + "tokens": null, + "indexing_status": "waiting", + "completed_at": null, + "updated_at": 1750464191, + "indexing_latency": null, + "error": null, + "enabled": true, + "disabled_at": null, + "disabled_by": null, + "archived": false, + "segment_count": 0, + "average_segment_length": 0, + "hit_count": null, + "display_status": "queuing", + "doc_form": "hierarchical_model", + "doc_language": "Chinese Simplified" + } + ``` + </CodeGroup> + </Col> +</Row> +___ +<hr className='ml-0 mr-0' /> + +<Heading + url='/datasets/{dataset_id}/documents/status/{action}' + method='PATCH' + title='Update Document Status' + name='#batch_document_status' +/> +<Row> + <Col> + ### Path + <Properties> + <Property name='dataset_id' type='string' key='dataset_id'> + Knowledge ID + </Property> + <Property name='action' type='string' key='action'> + - `enable` - Enable document + - `disable` - Disable document + - `archive` - Archive document + - `un_archive` - Unarchive document + </Property> + </Properties> + + ### Request Body + <Properties> + <Property name='document_ids' type='array[string]' key='document_ids'> + List of document IDs + </Property> + </Properties> + </Col> + <Col sticky> + <CodeGroup + title="Request" + tag="PATCH" + label="/datasets/{dataset_id}/documents/status/{action}" + targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "document_ids": ["doc-id-1", "doc-id-2"]\n}'`} + > + ```bash {{ title: 'cURL' }} + curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "document_ids": ["doc-id-1", "doc-id-2"] + }' + ``` + </CodeGroup> + + <CodeGroup title="Response"> + ```json {{ title: 'Response' }} + { + "result": "success" + } + ``` + </CodeGroup> + </Col> +</Row> + +<hr className='ml-0 mr-0' /> + <Heading url='/datasets/{dataset_id}/documents/{document_id}/segments' method='POST' diff --git a/web/app/(commonLayout)/datasets/template/template.ja.mdx b/web/app/(commonLayout)/datasets/template/template.ja.mdx index a796b65bae..23f78b5d7d 100644 --- a/web/app/(commonLayout)/datasets/template/template.ja.mdx +++ b/web/app/(commonLayout)/datasets/template/template.ja.mdx @@ -881,6 +881,187 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi <hr className='ml-0 mr-0' /> +<Heading + url='/datasets/{dataset_id}/documents/{document_id}' + method='GET' + title='ドキュメントの詳細を取得' + name='#get-document-detail' +/> +<Row> + <Col> + ドキュメントの詳細を取得. + ### Path + - `dataset_id` (string) ナレッジベースID + - `document_id` (string) ドキュメントID + + ### Query + - `metadata` (string) metadataのフィルター条件 `all`、`only`、または`without`。デフォルトは `all`。 + + ### Response + ナレッジベースドキュメントの詳細を返す. + </Col> + <Col sticky> + ### Request Example + <CodeGroup title="Request" tag="GET" label="/datasets/{dataset_id}/documents/{document_id}" targetCode={`curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \\\n-H 'Authorization: Bearer {api_key}'`}> + ```bash {{ title: 'cURL' }} + curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \ + -H 'Authorization: Bearer {api_key}' + ``` + </CodeGroup> + + ### Response Example + <CodeGroup title="Response"> + ```json {{ title: 'Response' }} + { + "id": "f46ae30c-5c11-471b-96d0-464f5f32a7b2", + "position": 1, + "data_source_type": "upload_file", + "data_source_info": { + "upload_file": { + ... + } + }, + "dataset_process_rule_id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_process_rule": { + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "document_process_rule": { + "id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_id": "48a0db76-d1a9-46c1-ae35-2baaa919a8a9", + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "name": "xxxx", + "created_from": "web", + "created_by": "17f71940-a7b5-4c77-b60f-2bd645c1ffa0", + "created_at": 1750464191, + "tokens": null, + "indexing_status": "waiting", + "completed_at": null, + "updated_at": 1750464191, + "indexing_latency": null, + "error": null, + "enabled": true, + "disabled_at": null, + "disabled_by": null, + "archived": false, + "segment_count": 0, + "average_segment_length": 0, + "hit_count": null, + "display_status": "queuing", + "doc_form": "hierarchical_model", + "doc_language": "Chinese Simplified" + } + ``` + </CodeGroup> + </Col> +</Row> +___ +<hr className='ml-0 mr-0' /> + + +<Heading + url='/datasets/{dataset_id}/documents/status/{action}' + method='PATCH' + title='ドキュメントステータスの更新' + name='#batch_document_status' +/> +<Row> + <Col> + ### パス + <Properties> + <Property name='dataset_id' type='string' key='dataset_id'> + ナレッジ ID + </Property> + <Property name='action' type='string' key='action'> + - `enable` - ドキュメントを有効化 + - `disable` - ドキュメントを無効化 + - `archive` - ドキュメントをアーカイブ + - `un_archive` - ドキュメントのアーカイブを解除 + </Property> + </Properties> + + ### リクエストボディ + <Properties> + <Property name='document_ids' type='array[string]' key='document_ids'> + ドキュメントIDのリスト + </Property> + </Properties> + </Col> + <Col sticky> + <CodeGroup + title="リクエスト" + tag="PATCH" + label="/datasets/{dataset_id}/documents/status/{action}" + targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "document_ids": ["doc-id-1", "doc-id-2"]\n}'`} + > + ```bash {{ title: 'cURL' }} + curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "document_ids": ["doc-id-1", "doc-id-2"] + }' + ``` + </CodeGroup> + + <CodeGroup title="レスポンス"> + ```json {{ title: 'Response' }} + { + "result": "success" + } + ``` + </CodeGroup> + </Col> +</Row> + +<hr className='ml-0 mr-0' /> + <Heading url='/datasets/{dataset_id}/documents/{document_id}/segments' method='POST' @@ -2413,3 +2594,4 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi </tbody> </table> <div className="pb-4" /> + diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index d121a93df2..c21ce3bf5f 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -55,7 +55,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi <Property name='indexing_technique' type='string' key='indexing_technique'> 索引方式 - <code>high_quality</code> 高质量:使用 - ding 模型进行嵌入,构建为向量数据库索引 + Embedding 模型进行嵌入,构建为向量数据库索引 - <code>economy</code> 经济:使用 keyword table index 的倒排索引进行构建 </Property> <Property name='doc_form' type='string' key='doc_form'> @@ -1131,6 +1131,187 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi <hr className='ml-0 mr-0' /> +<Heading + url='/datasets/{dataset_id}/documents/{document_id}' + method='GET' + title='获取文档详情' + name='#get-document-detail' +/> +<Row> + <Col> + 获取文档详情. + ### Path + - `dataset_id` (string) 知识库 ID + - `document_id` (string) 文档 ID + + ### Query + - `metadata` (string) metadata 过滤条件 `all`, `only`, 或者 `without`. 默认是 `all`. + + ### Response + 返回知识库文档的详情. + </Col> + <Col sticky> + ### Request Example + <CodeGroup title="Request" tag="GET" label="/datasets/{dataset_id}/documents/{document_id}" targetCode={`curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \\\n-H 'Authorization: Bearer {api_key}'`}> + ```bash {{ title: 'cURL' }} + curl -X GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}' \ + -H 'Authorization: Bearer {api_key}' + ``` + </CodeGroup> + + ### Response Example + <CodeGroup title="Response"> + ```json {{ title: 'Response' }} + { + "id": "f46ae30c-5c11-471b-96d0-464f5f32a7b2", + "position": 1, + "data_source_type": "upload_file", + "data_source_info": { + "upload_file": { + ... + } + }, + "dataset_process_rule_id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_process_rule": { + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "document_process_rule": { + "id": "24b99906-845e-499f-9e3c-d5565dd6962c", + "dataset_id": "48a0db76-d1a9-46c1-ae35-2baaa919a8a9", + "mode": "hierarchical", + "rules": { + "pre_processing_rules": [ + { + "id": "remove_extra_spaces", + "enabled": true + }, + { + "id": "remove_urls_emails", + "enabled": false + } + ], + "segmentation": { + "separator": "**********page_ending**********", + "max_tokens": 1024, + "chunk_overlap": 0 + }, + "parent_mode": "paragraph", + "subchunk_segmentation": { + "separator": "\n", + "max_tokens": 512, + "chunk_overlap": 0 + } + } + }, + "name": "xxxx", + "created_from": "web", + "created_by": "17f71940-a7b5-4c77-b60f-2bd645c1ffa0", + "created_at": 1750464191, + "tokens": null, + "indexing_status": "waiting", + "completed_at": null, + "updated_at": 1750464191, + "indexing_latency": null, + "error": null, + "enabled": true, + "disabled_at": null, + "disabled_by": null, + "archived": false, + "segment_count": 0, + "average_segment_length": 0, + "hit_count": null, + "display_status": "queuing", + "doc_form": "hierarchical_model", + "doc_language": "Chinese Simplified" + } + ``` + </CodeGroup> + </Col> +</Row> +___ +<hr className='ml-0 mr-0' /> + + +<Heading + url='/datasets/{dataset_id}/documents/status/{action}' + method='PATCH' + title='更新文档状态' + name='#batch_document_status' +/> +<Row> + <Col> + ### Path + <Properties> + <Property name='dataset_id' type='string' key='dataset_id'> + 知识库 ID + </Property> + <Property name='action' type='string' key='action'> + - `enable` - 启用文档 + - `disable` - 禁用文档 + - `archive` - 归档文档 + - `un_archive` - 取消归档文档 + </Property> + </Properties> + + ### Request Body + <Properties> + <Property name='document_ids' type='array[string]' key='document_ids'> + 文档ID列表 + </Property> + </Properties> + </Col> + <Col sticky> + <CodeGroup + title="Request" + tag="PATCH" + label="/datasets/{dataset_id}/documents/status/{action}" + targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "document_ids": ["doc-id-1", "doc-id-2"]\n}'`} + > + ```bash {{ title: 'cURL' }} + curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/status/{action}' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "document_ids": ["doc-id-1", "doc-id-2"] + }' + ``` + </CodeGroup> + + <CodeGroup title="Response"> + ```json {{ title: 'Response' }} + { + "result": "success" + } + ``` + </CodeGroup> + </Col> +</Row> + +<hr className='ml-0 mr-0' /> + <Heading url='/datasets/{dataset_id}/documents/{document_id}/segments' method='POST' diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index 8db336a17d..d057ba7599 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -12,17 +12,26 @@ const Layout: FC<{ }> = ({ children }) => { const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const pathname = usePathname() const searchParams = useSearchParams() const redirectUrl = searchParams.get('redirect_url') const [isLoading, setIsLoading] = useState(true) useEffect(() => { (async () => { + if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { + setIsLoading(false) + return + } + let appCode: string | null = null - if (redirectUrl) - appCode = redirectUrl?.split('/').pop() || null - else + if (redirectUrl) { + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + appCode = url.pathname.split('/').pop() || null + } + else { appCode = pathname.split('/').pop() || null + } if (!appCode) return @@ -31,7 +40,7 @@ const Layout: FC<{ setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) setIsLoading(false) })() - }, [pathname, redirectUrl, setWebAppAccessMode]) + }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) if (isLoading || isGlobalPending) { return <div className='flex h-full w-full items-center justify-center'> <Loading /> diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx index 9f9a8ad4e3..5e3f6fff1d 100644 --- a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -9,8 +9,7 @@ import Button from '@/app/components/base/button' import { changeWebAppPasswordWithToken } from '@/service/common' import Toast from '@/app/components/base/toast' import Input from '@/app/components/base/input' - -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ +import { validPassword } from '@/config' const ChangePasswordForm = () => { const { t } = useTranslation() diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index 1b8f18c98f..a2ba620ace 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -25,7 +25,10 @@ export default function CheckCode() { const redirectUrl = searchParams.get('redirect_url') const getAppCodeFromRedirectUrl = useCallback(() => { - const appCode = redirectUrl?.split('/').pop() + if (!redirectUrl) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + const appCode = url.pathname.split('/').pop() if (!appCode) return null @@ -62,7 +65,7 @@ export default function CheckCode() { localStorage.setItem('webapp_access_token', ret.data.access_token) const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token }) await setAccessToken(appCode, tokenResp.access_token) - router.replace(redirectUrl) + router.replace(decodeURIComponent(redirectUrl)) } } catch (error) { console.error(error) } diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx index e9b15ae331..612a9677a6 100644 --- a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx @@ -23,7 +23,10 @@ const ExternalMemberSSOAuth = () => { } const getAppCodeFromRedirectUrl = useCallback(() => { - const appCode = redirectUrl?.split('/').pop() + if (!redirectUrl) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + const appCode = url.pathname.split('/').pop() if (!appCode) return null diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index d9e56af1b8..2201b28a2f 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -1,3 +1,4 @@ +'use client' import Link from 'next/link' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -33,7 +34,10 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut const redirectUrl = searchParams.get('redirect_url') const getAppCodeFromRedirectUrl = useCallback(() => { - const appCode = redirectUrl?.split('/').pop() + if (!redirectUrl) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + const appCode = url.pathname.split('/').pop() if (!appCode) return null @@ -87,7 +91,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut localStorage.setItem('webapp_access_token', res.data.access_token) const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token }) await setAccessToken(appCode, tokenResp.access_token) - router.replace(redirectUrl) + router.replace(decodeURIComponent(redirectUrl)) } else { Toast.notify({ diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx index 5d649322ba..bcba572644 100644 --- a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx @@ -23,7 +23,10 @@ const SSOAuth: FC<SSOAuthProps> = ({ const redirectUrl = searchParams.get('redirect_url') const getAppCodeFromRedirectUrl = useCallback(() => { - const appCode = redirectUrl?.split('/').pop() + if (!redirectUrl) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + const appCode = url.pathname.split('/').pop() if (!appCode) return null diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index c12fde38dd..967516c416 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -23,10 +23,12 @@ const WebSSOForm: FC = () => { const redirectUrl = searchParams.get('redirect_url') const tokenFromUrl = searchParams.get('web_sso_token') const message = searchParams.get('message') + const code = searchParams.get('code') const getSigninUrl = useCallback(() => { const params = new URLSearchParams(searchParams) params.delete('message') + params.delete('code') return `/webapp-signin?${params.toString()}` }, [searchParams]) @@ -44,7 +46,10 @@ const WebSSOForm: FC = () => { } const getAppCodeFromRedirectUrl = useCallback(() => { - const appCode = redirectUrl?.split('/').pop() + if (!redirectUrl) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + const appCode = url.pathname.split('/').pop() if (!appCode) return null @@ -61,20 +66,20 @@ const WebSSOForm: FC = () => { localStorage.setItem('webapp_access_token', tokenFromUrl) const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) await setAccessToken(appCode, tokenResp.access_token) - router.replace(redirectUrl) + router.replace(decodeURIComponent(redirectUrl)) return } if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) await setAccessToken(appCode, tokenResp.access_token) - router.replace(redirectUrl) + router.replace(decodeURIComponent(redirectUrl)) } })() }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) useEffect(() => { if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) - router.replace(redirectUrl) + router.replace(decodeURIComponent(redirectUrl)) }, [webAppAccessMode, router, redirectUrl]) if (tokenFromUrl) { @@ -85,8 +90,8 @@ const WebSSOForm: FC = () => { if (message) { return <div className='flex h-full flex-col items-center justify-center gap-y-4'> - <AppUnavailable className='h-auto w-auto' code={t('share.common.appUnavailable')} unknownReason={message} /> - <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span> + <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> + <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> </div> } if (!redirectUrl) { diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index 19c1e44236..ffaecb79ef 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -21,6 +21,7 @@ import { IS_CE_EDITION } from '@/config' import Input from '@/app/components/base/input' import PremiumBadge from '@/app/components/base/premium-badge' import { useGlobalPublicStore } from '@/context/global-public-context' +import { validPassword } from '@/config' const titleClassName = ` system-sm-semibold text-text-secondary @@ -29,8 +30,6 @@ const descriptionClassName = ` mt-1 body-xs-regular text-text-tertiary ` -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ - export default function AccountPage() { const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 7b6e66f7e7..3817ebf5a4 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -10,7 +10,6 @@ import { RiFileCopy2Line, RiFileDownloadLine, RiFileUploadLine, - RiMoreLine, } from '@remixicon/react' import AppIcon from '../base/app-icon' import SwitchAppModal from '../app/switch-app-modal' @@ -35,20 +34,24 @@ import ContentDialog from '@/app/components/base/content-dialog' import Button from '@/app/components/base/button' import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView' import Divider from '../base/divider' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem' +import type { Operation } from './app-operations' +import AppOperations from './app-operations' export type IAppInfoProps = { expand: boolean + onlyShowDetail?: boolean + openState?: boolean + onDetailExpand?: (expand: boolean) => void } -const AppInfo = ({ expand }: IAppInfoProps) => { +const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailExpand }: IAppInfoProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const { replace } = useRouter() const { onPlanInfoChanged } = useProviderContext() const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(openState) const [showEditModal, setShowEditModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) @@ -183,53 +186,102 @@ const AppInfo = ({ expand }: IAppInfoProps) => { const { isCurrentWorkspaceEditor } = useAppContext() - const [showMore, setShowMore] = useState(false) - const handleTriggerMore = useCallback(() => { - setShowMore(true) - }, [setShowMore]) - if (!appDetail) return null + const operations = [ + { + id: 'edit', + title: t('app.editApp'), + icon: <RiEditLine />, + onClick: () => { + setOpen(false) + onDetailExpand?.(false) + setShowEditModal(true) + }, + }, + { + id: 'duplicate', + title: t('app.duplicate'), + icon: <RiFileCopy2Line />, + onClick: () => { + setOpen(false) + onDetailExpand?.(false) + setShowDuplicateModal(true) + }, + }, + { + id: 'export', + title: t('app.export'), + icon: <RiFileDownloadLine />, + onClick: exportCheck, + }, + (appDetail.mode !== 'agent-chat' && (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow')) ? { + id: 'import', + title: t('workflow.common.importDSL'), + icon: <RiFileUploadLine />, + onClick: () => { + setOpen(false) + onDetailExpand?.(false) + setShowImportDSLModal(true) + }, + } : undefined, + (appDetail.mode !== 'agent-chat' && (appDetail.mode === 'completion' || appDetail.mode === 'chat')) ? { + id: 'switch', + title: t('app.switch'), + icon: <RiExchange2Line />, + onClick: () => { + setOpen(false) + onDetailExpand?.(false) + setShowSwitchModal(true) + }, + } : undefined, + ].filter((op): op is Operation => Boolean(op)) + return ( <div> - <button - onClick={() => { - if (isCurrentWorkspaceEditor) - setOpen(v => !v) - }} - className='block w-full' - > - <div className={cn('flex rounded-lg', expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}> - <div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}> - <AppIcon - size={expand ? 'large' : 'small'} - iconType={appDetail.icon_type} - icon={appDetail.icon} - background={appDetail.icon_background} - imageUrl={appDetail.icon_url} - /> - <div className='flex items-center justify-center rounded-md p-0.5'> - <div className='flex h-5 w-5 items-center justify-center'> - <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> + {!onlyShowDetail && ( + <button + onClick={() => { + if (isCurrentWorkspaceEditor) + setOpen(v => !v) + }} + className='block w-full' + > + <div className={cn('flex rounded-lg', expand ? 'flex-col gap-2 p-2 pb-2.5' : 'items-start justify-center gap-1 p-1', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}> + <div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}> + <AppIcon + size={expand ? 'large' : 'small'} + iconType={appDetail.icon_type} + icon={appDetail.icon} + background={appDetail.icon_background} + imageUrl={appDetail.icon_url} + /> + <div className='flex items-center justify-center rounded-md p-0.5'> + <div className='flex h-5 w-5 items-center justify-center'> + <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> + </div> </div> </div> - </div> - { - expand && ( - <div className='flex flex-col items-start gap-1'> - <div className='flex w-full'> - <div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div> + { + expand && ( + <div className='flex flex-col items-start gap-1'> + <div className='flex w-full'> + <div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div> + </div> + <div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div> </div> - <div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div> - </div> - ) - } - </div> - </button> + ) + } + </div> + </button> + )} <ContentDialog - show={open} - onClose={() => setOpen(false)} + show={onlyShowDetail ? openState : open} + onClose={() => { + setOpen(false) + onDetailExpand?.(false) + }} className='absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0' > <div className='flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4'> @@ -248,95 +300,19 @@ const AppInfo = ({ expand }: IAppInfoProps) => { </div> {/* description */} {appDetail.description && ( - <div className='system-xs-regular overflow-wrap-anywhere w-full max-w-full whitespace-normal break-words text-text-tertiary'>{appDetail.description}</div> + <div className='system-xs-regular overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal break-words text-text-tertiary'>{appDetail.description}</div> )} {/* operations */} - <div className='flex flex-wrap items-center gap-1 self-stretch'> - <Button - size={'small'} - variant={'secondary'} - className='gap-[1px]' - onClick={() => { - setOpen(false) - setShowEditModal(true) - }} - > - <RiEditLine className='h-3.5 w-3.5 text-components-button-secondary-text' /> - <span className='system-xs-medium text-components-button-secondary-text'>{t('app.editApp')}</span> - </Button> - <Button - size={'small'} - variant={'secondary'} - className='gap-[1px]' - onClick={() => { - setOpen(false) - setShowDuplicateModal(true) - }}> - <RiFileCopy2Line className='h-3.5 w-3.5 text-components-button-secondary-text' /> - <span className='system-xs-medium text-components-button-secondary-text'>{t('app.duplicate')}</span> - </Button> - <Button - size={'small'} - variant={'secondary'} - className='gap-[1px]' - onClick={exportCheck} - > - <RiFileDownloadLine className='h-3.5 w-3.5 text-components-button-secondary-text' /> - <span className='system-xs-medium text-components-button-secondary-text'>{t('app.export')}</span> - </Button> - {appDetail.mode !== 'agent-chat' && <PortalToFollowElem - open={showMore} - onOpenChange={setShowMore} - placement='bottom-end' - offset={{ - mainAxis: 4, - }}> - <PortalToFollowElemTrigger onClick={handleTriggerMore}> - <Button - size={'small'} - variant={'secondary'} - className='gap-[1px]' - > - <RiMoreLine className='h-3.5 w-3.5 text-components-button-secondary-text' /> - <span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span> - </Button> - </PortalToFollowElemTrigger> - <PortalToFollowElemContent className='z-[21]'> - <div className='flex w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'> - { - (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') - && <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover' - onClick={() => { - setOpen(false) - setShowImportDSLModal(true) - }}> - <RiFileUploadLine className='h-4 w-4 text-text-tertiary' /> - <span className='system-md-regular text-text-secondary'>{t('workflow.common.importDSL')}</span> - </div> - } - { - (appDetail.mode === 'completion' || appDetail.mode === 'chat') - && <div className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover' - onClick={() => { - setOpen(false) - setShowSwitchModal(true) - }}> - <RiExchange2Line className='h-4 w-4 text-text-tertiary' /> - <span className='system-md-regular text-text-secondary'>{t('app.switch')}</span> - </div> - } - </div> - </PortalToFollowElemContent> - </PortalToFollowElem>} - </div> - </div> - <div className='flex flex-1'> - <CardView - appId={appDetail.id} - isInPanel={true} - className='flex grow flex-col gap-2 overflow-auto px-2 py-1' + <AppOperations + gap={4} + operations={operations} /> </div> + <CardView + appId={appDetail.id} + isInPanel={true} + className='flex flex-1 flex-col gap-2 overflow-auto px-2 py-1' + /> <Divider /> <div className='flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch border-t-[0.5px] border-divider-subtle p-2'> <Button @@ -345,6 +321,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { className='gap-0.5' onClick={() => { setOpen(false) + onDetailExpand?.(false) setShowConfirmDelete(true) }} > diff --git a/web/app/components/app-sidebar/app-operations.tsx b/web/app/components/app-sidebar/app-operations.tsx new file mode 100644 index 0000000000..49cad71573 --- /dev/null +++ b/web/app/components/app-sidebar/app-operations.tsx @@ -0,0 +1,145 @@ +import type { ReactElement } from 'react' +import { cloneElement, useCallback } from 'react' +import { useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem' +import { RiMoreLine } from '@remixicon/react' + +export type Operation = { + id: string; title: string; icon: ReactElement; onClick: () => void +} + +const AppOperations = ({ operations, gap }: { + operations: Operation[] + gap: number +}) => { + const { t } = useTranslation() + const [visibleOpreations, setVisibleOperations] = useState<Operation[]>([]) + const [moreOperations, setMoreOperations] = useState<Operation[]>([]) + const [showMore, setShowMore] = useState(false) + const navRef = useRef<HTMLDivElement>(null) + const handleTriggerMore = useCallback(() => { + setShowMore(true) + }, [setShowMore]) + + useEffect(() => { + const moreElement = document.getElementById('more') + const navElement = document.getElementById('nav') + let width = 0 + const containerWidth = navElement?.clientWidth ?? 0 + const moreWidth = moreElement?.clientWidth ?? 0 + + if (containerWidth === 0 || moreWidth === 0) return + + const updatedEntries: Record<string, boolean> = operations.reduce((pre, cur) => { + pre[cur.id] = false + return pre + }, {} as Record<string, boolean>) + const childrens = Array.from(navRef.current!.children).slice(0, -1) + for (let i = 0; i < childrens.length; i++) { + const child: any = childrens[i] + const id = child.dataset.targetid + if (!id) break + const childWidth = child.clientWidth + + if (width + gap + childWidth + moreWidth <= containerWidth) { + updatedEntries[id] = true + width += gap + childWidth + } + else { + if (i === childrens.length - 1 && width + childWidth <= containerWidth) + updatedEntries[id] = true + else + updatedEntries[id] = false + break + } + } + setVisibleOperations(operations.filter(item => updatedEntries[item.id])) + setMoreOperations(operations.filter(item => !updatedEntries[item.id])) + }, [operations, gap]) + + return ( + <> + {!visibleOpreations.length && <div + id="nav" + ref={navRef} + className="flex h-0 items-center self-stretch overflow-hidden" + style={{ gap }} + > + {operations.map((operation, index) => + <Button + key={index} + data-targetid={operation.id} + size={'small'} + variant={'secondary'} + className="gap-[1px]"> + {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} + <span className="system-xs-medium text-components-button-secondary-text"> + {operation.title} + </span> + </Button>, + )} + <Button + id="more" + size={'small'} + variant={'secondary'} + className="gap-[1px]" + > + <RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" /> + <span className="system-xs-medium text-components-button-secondary-text"> + {t('common.operation.more')} + </span> + </Button> + </div>} + <div className="flex items-center self-stretch overflow-hidden" style={{ gap }}> + {visibleOpreations.map(operation => + <Button + key={operation.id} + data-targetid={operation.id} + size={'small'} + variant={'secondary'} + className="gap-[1px]" + onClick={operation.onClick}> + {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })} + <span className="system-xs-medium text-components-button-secondary-text"> + {operation.title} + </span> + </Button>, + )} + {visibleOpreations.length < operations.length && <PortalToFollowElem + open={showMore} + onOpenChange={setShowMore} + placement='bottom-end' + offset={{ + mainAxis: 4, + }}> + <PortalToFollowElemTrigger onClick={handleTriggerMore}> + <Button + size={'small'} + variant={'secondary'} + className='gap-[1px]' + > + <RiMoreLine className='h-3.5 w-3.5 text-components-button-secondary-text' /> + <span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span> + </Button> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-[21]'> + <div className='flex min-w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'> + {moreOperations.map(item => <div + key={item.id} + className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover' + onClick={item.onClick} + > + {cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })} + <span className='system-md-regular text-text-secondary'>{item.title}</span> + </div>)} + </div> + </PortalToFollowElemContent> + </PortalToFollowElem>} + </div> + </> + ) +} + +export default AppOperations diff --git a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx new file mode 100644 index 0000000000..b1da43ae14 --- /dev/null +++ b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx @@ -0,0 +1,125 @@ +import React, { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppContext } from '@/context/app-context' +import { + RiEqualizer2Line, + RiMenuLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import AppIcon from '../base/app-icon' +import Divider from '../base/divider' +import AppInfo from './app-info' +import NavLink from './navLink' +import { useStore as useAppStore } from '@/app/components/app/store' +import type { NavIcon } from './navLink' +import cn from '@/utils/classnames' + +type Props = { + navigation: Array<{ + name: string + href: string + icon: NavIcon + selectedIcon: NavIcon + }> +} + +const AppSidebarDropdown = ({ navigation }: Props) => { + const { t } = useTranslation() + const { isCurrentWorkspaceEditor } = useAppContext() + const appDetail = useAppStore(state => state.appDetail) + const [detailExpand, setDetailExpand] = useState(false) + + const [open, doSetOpen] = useState(false) + const openRef = useRef(open) + const setOpen = useCallback((v: boolean) => { + doSetOpen(v) + openRef.current = v + }, [doSetOpen]) + const handleTrigger = useCallback(() => { + setOpen(!openRef.current) + }, [setOpen]) + + if (!appDetail) + return null + + return ( + <> + <div className='fixed left-2 top-2 z-20'> + <PortalToFollowElem + open={open} + onOpenChange={setOpen} + placement='bottom-start' + offset={{ + mainAxis: -41, + }} + > + <PortalToFollowElemTrigger onClick={handleTrigger}> + <div className={cn('flex cursor-pointer items-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-1 shadow-lg backdrop-blur-sm hover:bg-background-default-hover', open && 'bg-background-default-hover')}> + <AppIcon + size='small' + iconType={appDetail.icon_type} + icon={appDetail.icon} + background={appDetail.icon_background} + imageUrl={appDetail.icon_url} + /> + <RiMenuLine className='h-4 w-4 text-text-tertiary' /> + </div> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-[1000]'> + <div className={cn('w-[305px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg')}> + <div className='p-2'> + <div + className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')} + onClick={() => { + setDetailExpand(true) + setOpen(false) + }} + > + <div className='flex items-center justify-between self-stretch'> + <AppIcon + size='large' + iconType={appDetail.icon_type} + icon={appDetail.icon} + background={appDetail.icon_background} + imageUrl={appDetail.icon_url} + /> + <div className='flex items-center justify-center rounded-md p-0.5'> + <div className='flex h-5 w-5 items-center justify-center'> + <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> + </div> + </div> + </div> + <div className='flex flex-col items-start gap-1'> + <div className='flex w-full'> + <div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div> + </div> + <div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div> + </div> + </div> + </div> + <div className='px-4'> + <Divider bgStyle='gradient' /> + </div> + <nav className='space-y-0.5 px-3 pb-6 pt-4'> + {navigation.map((item, index) => { + return ( + <NavLink key={index} mode='expand' iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} /> + ) + })} + </nav> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + </div> + <div className='z-20'> + <AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} /> + </div> + </> + ) +} + +export default AppSidebarDropdown diff --git a/web/app/components/app-sidebar/basic.tsx b/web/app/components/app-sidebar/basic.tsx index 6a7d5a13c2..00357d6c27 100644 --- a/web/app/components/app-sidebar/basic.tsx +++ b/web/app/components/app-sidebar/basic.tsx @@ -2,6 +2,10 @@ import React from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '../base/app-icon' import Tooltip from '@/app/components/base/tooltip' +import { + Code, + WindowCursor, +} from '@/app/components/base/icons/src/vender/workflow' export type IAppBasicProps = { iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' @@ -14,25 +18,13 @@ export type IAppBasicProps = { textStyle?: { main?: string; extra?: string } isExtraInLine?: boolean mode?: string + hideType?: boolean } -const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> - <path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> -</svg> - const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" /> </svg> -const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> -</svg> - const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clipPath="url(#clip0_6294_13848)"> <path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" /> @@ -48,13 +40,17 @@ const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xm const ICON_MAP = { app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />, - api: <AppIcon innerIcon={ApiSvg} className='border !border-purple-200 !bg-purple-50' />, + api: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'> + <Code className='h-4 w-4 text-text-primary-on-surface' /> + </div>, dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />, - webapp: <AppIcon innerIcon={WebappSvg} className='border !border-primary-200 !bg-primary-100' />, + webapp: <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'> + <WindowCursor className='h-4 w-4 text-text-primary-on-surface' /> +</div>, notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />, } -export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app' }: IAppBasicProps) { +export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, isExtraInLine, mode = 'expand', iconType = 'app', hideType }: IAppBasicProps) { const { t } = useTranslation() return ( @@ -88,9 +84,10 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type /> } </div> - {isExtraInLine ? ( + {!hideType && isExtraInLine && ( <div className="system-2xs-medium-uppercase flex text-text-tertiary">{type}</div> - ) : ( + )} + {!hideType && !isExtraInLine && ( <div className='system-2xs-medium-uppercase text-text-tertiary'>{isExternal ? t('dataset.externalTag') : type}</div> )} </div>} diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index f58985ed96..b6bfc0e9ac 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -1,4 +1,5 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import { usePathname } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { RiLayoutLeft2Line, RiLayoutRight2Line } from '@remixicon/react' import NavLink from './navLink' @@ -6,8 +7,10 @@ import type { NavIcon } from './navLink' import AppBasic from './basic' import AppInfo from './app-info' import DatasetInfo from './dataset-info' +import AppSidebarDropdown from './app-sidebar-dropdown' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useStore as useAppStore } from '@/app/components/app/store' +import { useEventEmitterContextContext } from '@/context/event-emitter' import cn from '@/utils/classnames' export type IAppDetailNavProps = { @@ -39,6 +42,18 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati setAppSiderbarExpand(state === 'expand' ? 'collapse' : 'expand') } + // // Check if the current path is a workflow canvas & fullscreen + const pathname = usePathname() + const inWorkflowCanvas = pathname.endsWith('/workflow') + const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) + const { eventEmitter } = useEventEmitterContextContext() + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === 'workflow-canvas-maximize') + setHideHeader(v.payload) + }) + useEffect(() => { if (appSidebarExpand) { localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand) @@ -46,6 +61,14 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati } }, [appSidebarExpand, setAppSiderbarExpand]) + if (inWorkflowCanvas && hideHeader) { + return ( + <div className='flex w-0 shrink-0'> + <AppSidebarDropdown navigation={navigation} /> + </div> + ) +} + return ( <div className={` diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 5825bb72ee..83a7ffd553 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -278,7 +278,7 @@ const AppPublisher = ({ onClick={() => { setShowAppAccessControl(true) }}> - <div className='flex grow items-center gap-x-1.5 pr-1'> + <div className='flex grow items-center gap-x-1.5 overflow-hidden pr-1'> {appDetail?.access_mode === AccessMode.ORGANIZATION && <> <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> @@ -288,7 +288,9 @@ const AppPublisher = ({ {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <> <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> - <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p> + <div className='grow truncate'> + <span className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</span> + </div> </> } {appDetail?.access_mode === AccessMode.PUBLIC @@ -312,10 +314,10 @@ const AppPublisher = ({ {!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>} </div>} <div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'> - <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> + <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> <SuggestedAction className='flex-1' - disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)} + disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail?.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)} link={appURL} icon={<RiPlayCircleLine className='h-4 w-4' />} > @@ -324,10 +326,10 @@ const AppPublisher = ({ </Tooltip> {appDetail?.mode === 'workflow' || appDetail?.mode === 'completion' ? ( - <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> + <Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}> <SuggestedAction className='flex-1' - disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)} + disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)} link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`} icon={<RiPlayList2Line className='h-4 w-4' />} > diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 1eec51992d..437e25fde4 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -227,7 +227,7 @@ const AdvancedPromptInput: FC<Props> = ({ }} variableBlock={{ show: true, - variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ + variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api' && item.key && item.key.trim() && item.name && item.name.trim()).map(item => ({ name: item.name, value: item.key, })), diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx index 592c95261c..94a02945bb 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx @@ -1,13 +1,11 @@ 'use client' import type { FC } from 'react' import React from 'react' -import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import Panel from '@/app/components/app/configuration/base/feature-panel' import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { showWarning: boolean @@ -19,7 +17,7 @@ const HistoryPanel: FC<Props> = ({ onShowEditModal, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() return ( <Panel @@ -45,9 +43,8 @@ const HistoryPanel: FC<Props> = ({ {showWarning && ( <div className='flex justify-between rounded-b-xl bg-background-section-burn px-3 py-2 text-xs text-text-secondary'> <div>{t('appDebug.feature.conversationHistory.tip')} - <a href={`${locale === LanguagesSupported[1] - ? 'https://docs.dify.ai/zh-hans/learn-more/extended-reading/prompt-engineering/README' - : 'https://docs.dify.ai/en/features/prompt-engineering'}`} + <a href={docLink('/learn-more/extended-reading/what-is-llmops', + { 'zh-Hans': '/learn-more/extended-reading/prompt-engineering/README' })} target='_blank' rel='noopener noreferrer' className='text-[#155EEF]'>{t('appDebug.feature.conversationHistory.learnMore')} </a> diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 3268c1dc76..eb0f524386 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -10,7 +10,6 @@ import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' import cn from '@/utils/classnames' import type { PromptVariable } from '@/models/debug' import Tooltip from '@/app/components/base/tooltip' -import type { CompletionParams } from '@/types/app' import { AppType } from '@/types/app' import { getNewVar, getVars } from '@/utils/var' import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn' @@ -63,7 +62,6 @@ const Prompt: FC<ISimplePromptInput> = ({ const { eventEmitter } = useEventEmitterContextContext() const { modelConfig, - completionParams, dataSets, setModelConfig, setPrevPromptConfig, @@ -99,20 +97,31 @@ const Prompt: FC<ISimplePromptInput> = ({ }, }) } - const promptVariablesObj = (() => { - const obj: Record<string, boolean> = {} - promptVariables.forEach((item) => { - obj[item.key] = true - }) - return obj - })() const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables) const [newTemplates, setNewTemplates] = React.useState('') const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false) const handleChange = (newTemplates: string, keys: string[]) => { - const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.find(item => item.variable === key)).map(key => getNewVar(key, '')) + // Filter out keys that are not properly defined (either not exist or exist but without valid name) + const newPromptVariables = keys.filter((key) => { + // Check if key exists in external data tools + if (externalDataToolsConfig.find((item: ExternalDataTool) => item.variable === key)) + return false + + // Check if key exists in prompt variables + const existingVar = promptVariables.find((item: PromptVariable) => item.key === key) + if (!existingVar) { + // Variable doesn't exist at all + return true + } + + // Variable exists but check if it has valid name and key + return !existingVar.name || !existingVar.name.trim() || !existingVar.key || !existingVar.key.trim() + + return false + }).map(key => getNewVar(key, '')) + if (newPromptVariables.length > 0) { setNewPromptVariables(newPromptVariables) setNewTemplates(newTemplates) @@ -212,14 +221,14 @@ const Prompt: FC<ISimplePromptInput> = ({ }} variableBlock={{ show: true, - variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ + variables: modelConfig.configs.prompt_variables.filter((item: PromptVariable) => item.type !== 'api' && item.key && item.key.trim() && item.name && item.name.trim()).map((item: PromptVariable) => ({ name: item.name, value: item.key, })), }} externalToolBlock={{ show: true, - externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({ + externalTools: modelConfig.configs.prompt_variables.filter((item: PromptVariable) => item.type === 'api').map((item: PromptVariable) => ({ name: item.name, variableName: item.key, icon: item.icon, @@ -264,14 +273,6 @@ const Prompt: FC<ISimplePromptInput> = ({ {showAutomatic && ( <GetAutomaticResModal mode={mode as AppType} - model={ - { - provider: modelConfig.provider, - name: modelConfig.model_id, - mode: modelConfig.mode, - completion_params: completionParams as CompletionParams, - } - } isShow={showAutomatic} onClose={showAutomaticFalse} onFinished={handleAutomaticRes} diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 29cbc55b90..8fcc0f4c08 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import type { ChangeEvent, FC } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -11,7 +11,7 @@ import SelectTypeItem from '../select-type-item' import Field from './field' import Input from '@/app/components/base/input' import Toast from '@/app/components/base/toast' -import { checkKeys, getNewVarInWorkflow } from '@/utils/var' +import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' import ConfigContext from '@/context/debug-configuration' import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types' import Modal from '@/app/components/base/modal' @@ -109,6 +109,20 @@ const ConfigModal: FC<IConfigModalProps> = ({ }) }, [checkVariableName, tempPayload.label]) + const handleVarNameChange = useCallback((e: ChangeEvent<any>) => { + replaceSpaceWithUnderscreInVarNameInput(e.target) + const value = e.target.value + const { isValid, errorKey, errorMessageKey } = checkKeys([value], true) + if (!isValid) { + Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + }) + return + } + handlePayloadChange('variable')(e.target.value) + }, [handlePayloadChange, t]) + const handleConfirm = () => { const moreInfo = tempPayload.variable === payload?.variable ? undefined @@ -200,7 +214,7 @@ const ConfigModal: FC<IConfigModalProps> = ({ <Field title={t('appDebug.variableConfig.varName')}> <Input value={variable} - onChange={e => handlePayloadChange('variable')(e.target.value)} + onChange={handleVarNameChange} onBlur={handleVarKeyBlur} placeholder={t('appDebug.variableConfig.inputPlaceholder')!} /> diff --git a/web/app/components/app/configuration/config-var/select-var-type.tsx b/web/app/components/app/configuration/config-var/select-var-type.tsx index f82e931882..485f9932f3 100644 --- a/web/app/components/app/configuration/config-var/select-var-type.tsx +++ b/web/app/components/app/configuration/config-var/select-var-type.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import cn from '@/utils/classnames' import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import { PortalToFollowElem, @@ -28,11 +27,11 @@ type ItemProps = { const SelectItem: FC<ItemProps> = ({ text, type, value, Icon, onClick }) => { return ( <div - className='flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-gray-50' + className='flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover' onClick={() => onClick(value)} > - {Icon ? <Icon className='h-4 w-4 text-gray-500' /> : <InputVarTypeIcon type={type!} className='h-4 w-4 text-gray-500' />} - <div className='ml-2 truncate text-xs text-gray-600'>{text}</div> + {Icon ? <Icon className='h-4 w-4 text-text-secondary' /> : <InputVarTypeIcon type={type!} className='h-4 w-4 text-text-secondary' />} + <div className='ml-2 truncate text-xs text-text-primary'>{text}</div> </div> ) } @@ -57,17 +56,17 @@ const SelectVarType: FC<Props> = ({ }} > <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> - <OperationBtn type='add' className={cn(open && 'bg-gray-200')} /> + <OperationBtn type='add' /> </PortalToFollowElemTrigger> <PortalToFollowElemContent style={{ zIndex: 1000 }}> - <div className='min-w-[192px] rounded-lg border border-gray-200 bg-white shadow-lg'> + <div className='min-w-[192px] rounded-lg border border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'> <div className='p-1'> <SelectItem type={InputVarType.textInput} value='string' text={t('appDebug.variableConfig.string')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.paragraph} value='paragraph' text={t('appDebug.variableConfig.paragraph')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.select} value='select' text={t('appDebug.variableConfig.select')} onClick={handleChange}></SelectItem> <SelectItem type={InputVarType.number} value='number' text={t('appDebug.variableConfig.number')} onClick={handleChange}></SelectItem> </div> - <div className='h-[1px] bg-gray-100'></div> + <div className='h-[1px] border-t border-components-panel-border'></div> <div className='p-1'> <SelectItem Icon={ApiConnection} value='api' text={t('appDebug.variableConfig.apiBasedVar')} onClick={handleChange}></SelectItem> </div> diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index a3149447d4..a1b82ab2fe 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -30,15 +30,31 @@ import ConfigCredential from '@/app/components/tools/setting/build-in/config-cre import { updateBuiltInToolCredential } from '@/service/tools' import cn from '@/utils/classnames' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' -import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' +import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' import { canFindTool } from '@/utils' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' +import type { ToolWithProvider } from '@/app/components/workflow/types' import { useMittContextSelector } from '@/context/mitt-context' type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null const AgentTools: FC = () => { const { t } = useTranslation() const [isShowChooseTool, setIsShowChooseTool] = useState(false) - const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext) + const { modelConfig, setModelConfig } = useContext(ConfigContext) + const { data: buildInTools } = useAllBuiltInTools() + const { data: customTools } = useAllCustomTools() + const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() + const collectionList = useMemo(() => { + const allTools = [ + ...(buildInTools || []), + ...(customTools || []), + ...(workflowTools || []), + ...(mcpTools || []), + ] + return allTools + }, [buildInTools, customTools, workflowTools, mcpTools]) + const formattingChangedDispatcher = useFormattingChangedDispatcher() const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null) const currentCollection = useMemo(() => { @@ -96,23 +112,38 @@ const AgentTools: FC = () => { } const [isDeleting, setIsDeleting] = useState<number>(-1) - + const getToolValue = (tool: ToolDefaultValue) => { + return { + provider_id: tool.provider_id, + provider_type: tool.provider_type as CollectionType, + provider_name: tool.provider_name, + tool_name: tool.tool_name, + tool_label: tool.tool_label, + tool_parameters: tool.params, + notAuthor: !tool.is_team_authorization, + enabled: true, + } + } const handleSelectTool = (tool: ToolDefaultValue) => { const newModelConfig = produce(modelConfig, (draft) => { - draft.agentConfig.tools.push({ - provider_id: tool.provider_id, - provider_type: tool.provider_type as CollectionType, - provider_name: tool.provider_name, - tool_name: tool.tool_name, - tool_label: tool.tool_label, - tool_parameters: tool.params, - notAuthor: !tool.is_team_authorization, - enabled: true, - }) + draft.agentConfig.tools.push(getToolValue(tool)) }) setModelConfig(newModelConfig) } + const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => { + const newModelConfig = produce(modelConfig, (draft) => { + draft.agentConfig.tools.push(...tool.map(getToolValue)) + }) + setModelConfig(newModelConfig) + } + const getProviderShowName = (item: AgentTool) => { + const type = item.provider_type + if(type === CollectionType.builtIn) + return item.provider_name.split('/').pop() + return item.provider_name + } + return ( <> <Panel @@ -143,7 +174,9 @@ const AgentTools: FC = () => { disabled={false} supportAddCustomTool onSelect={handleSelectTool} - selectedTools={tools as any} + onSelectMultiple={handleSelectMultipleTool} + selectedTools={tools as unknown as ToolValue[]} + canChooseMCPTool /> </> )} @@ -161,7 +194,7 @@ const AgentTools: FC = () => { <div className='flex w-0 grow items-center'> {item.isDeleted && <DefaultToolIcon className='h-5 w-5' />} {!item.isDeleted && ( - <div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}> + <div className={cn((item.notAuthor || !item.enabled) && 'shrink-0 opacity-50')}> {typeof item.icon === 'string' && <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${item.icon})` }} />} {typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />} </div> @@ -172,11 +205,10 @@ const AgentTools: FC = () => { (item.isDeleted || item.notAuthor || !item.enabled) ? 'opacity-50' : '', )} > - <span className='system-xs-medium pr-1.5 text-text-secondary'>{item.provider_type === CollectionType.builtIn ? item.provider_name.split('/').pop() : item.tool_label}</span> + <span className='system-xs-medium pr-1.5 text-text-secondary'>{getProviderShowName(item)}</span> <span className='text-text-tertiary'>{item.tool_label}</span> {!item.isDeleted && ( <Tooltip - needsDelay popupContent={ <div className='w-[180px]'> <div className='mb-1.5 text-text-secondary'>{item.tool_name}</div> @@ -199,7 +231,6 @@ const AgentTools: FC = () => { <div className='mr-2 flex items-center'> <Tooltip popupContent={t('tools.toolRemoved')} - needsDelay > <div className='mr-1 cursor-pointer rounded-md p-1 hover:bg-black/5'> <AlertTriangle className='h-4 w-4 text-[#F79009]' /> @@ -226,7 +257,6 @@ const AgentTools: FC = () => { {!item.notAuthor && ( <Tooltip popupContent={t('tools.setBuiltInTools.infoAndSetting')} - needsDelay > <div className='cursor-pointer rounded-md p-1 hover:bg-black/5' onClick={() => { setCurrentTool(item) @@ -285,8 +315,7 @@ const AgentTools: FC = () => { <SettingBuiltInTool toolName={currentTool?.tool_name as string} setting={currentTool?.tool_parameters} - collection={currentTool?.collection as Collection} - isBuiltIn={currentTool?.collection?.type === CollectionType.builtIn} + collection={currentTool?.collection as ToolWithProvider} isModel={currentTool?.collection?.type === CollectionType.model} onSave={handleToolSettingChange} onHide={() => setIsShowSettingTool(false)} diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index 952ad66fc4..1ad814c6e9 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -24,10 +24,11 @@ import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWor import I18n from '@/context/i18n' import { getLanguage } from '@/i18n/language' import cn from '@/utils/classnames' +import type { ToolWithProvider } from '@/app/components/workflow/types' type Props = { showBackButton?: boolean - collection: Collection + collection: Collection | ToolWithProvider isBuiltIn?: boolean isModel?: boolean toolName: string @@ -51,9 +52,10 @@ const SettingBuiltInTool: FC<Props> = ({ const { locale } = useContext(I18n) const language = getLanguage(locale) const { t } = useTranslation() - - const [isLoading, setIsLoading] = useState(true) - const [tools, setTools] = useState<Tool[]>([]) + const passedTools = (collection as ToolWithProvider).tools + const hasPassedTools = passedTools?.length > 0 + const [isLoading, setIsLoading] = useState(!hasPassedTools) + const [tools, setTools] = useState<Tool[]>(hasPassedTools ? passedTools : []) const currTool = tools.find(tool => tool.name === toolName) const formSchemas = currTool ? toolParametersToFormSchemas(currTool.parameters) : [] const infoSchemas = formSchemas.filter(item => item.form === 'llm') @@ -63,7 +65,7 @@ const SettingBuiltInTool: FC<Props> = ({ const [currType, setCurrType] = useState('info') const isInfoActive = currType === 'info' useEffect(() => { - if (!collection) + if (!collection || hasPassedTools) return (async () => { diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index 7f7f140eda..579b7c4d64 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -107,7 +107,7 @@ const Editor: FC<Props> = ({ }} variableBlock={{ show: true, - variables: modelConfig.configs.prompt_variables.map(item => ({ + variables: modelConfig.configs.prompt_variables.filter(item => item.key && item.key.trim() && item.name && item.name.trim()).map(item => ({ name: item.name, value: item.key, })), diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index e4b333d998..aacaa81ac2 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import { @@ -22,7 +22,7 @@ import Textarea from '@/app/components/base/textarea' import Toast from '@/app/components/base/toast' import { generateRule } from '@/service/debug' import ConfigPrompt from '@/app/components/app/configuration/config-prompt' -import type { Model } from '@/types/app' +import type { CompletionParams, Model } from '@/types/app' import { AppType } from '@/types/app' import ConfigVar from '@/app/components/app/configuration/config-var' import GroupName from '@/app/components/app/configuration/base/group-name' @@ -33,14 +33,15 @@ import { LoveMessage } from '@/app/components/base/icons/src/vender/features' // type import type { AutomaticRes } from '@/service/debug' import { Generator } from '@/app/components/base/icons/src/vender/other' -import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' -import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' +import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' + import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' +import type { ModelModeType } from '@/types/app' +import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' export type IGetAutomaticResProps = { mode: AppType - model: Model isShow: boolean onClose: () => void onFinished: (res: AutomaticRes) => void @@ -65,16 +66,23 @@ const TryLabel: FC<{ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ mode, - model, isShow, onClose, isInLLMNode, onFinished, }) => { const { t } = useTranslation() + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model + : null + const [model, setModel] = React.useState<Model>(localModel || { + name: '', + provider: '', + mode: mode as unknown as ModelModeType.chat, + completion_params: {} as CompletionParams, + }) const { - currentProvider, - currentModel, + defaultModel, } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) const tryList = [ { @@ -115,7 +123,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ }, ] - const [instruction, setInstruction] = React.useState<string>('') + const [instruction, setInstruction] = useState<string>('') const handleChooseTemplate = useCallback((key: string) => { return () => { const template = t(`appDebug.generate.template.${key}.instruction`) @@ -135,7 +143,25 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ return true } const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) - const [res, setRes] = React.useState<AutomaticRes | null>(null) + const [res, setRes] = useState<AutomaticRes | null>(null) + + useEffect(() => { + if (defaultModel) { + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') || '') + : null + if (localModel) { + setModel(localModel) + } + else { + setModel(prev => ({ + ...prev, + name: defaultModel.model, + provider: defaultModel.provider.provider, + })) + } + } + }, [defaultModel]) const renderLoading = ( <div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3'> @@ -154,6 +180,26 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ </div> ) + const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => { + const newModel = { + ...model, + provider: newValue.provider, + name: newValue.modelId, + mode: newValue.mode as ModelModeType, + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) + + const handleCompletionParamsChange = useCallback((newParams: FormValue) => { + const newModel = { + ...model, + completion_params: newParams as CompletionParams, + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) + const onGenerate = async () => { if (!isValid()) return @@ -198,17 +244,18 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ <div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div> <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div> </div> - <div className='mb-8 flex items-center'> - <ModelIcon - className='mr-1.5 shrink-0 ' - provider={currentProvider} - modelName={currentModel?.model} - /> - <ModelName - className='grow' - modelItem={currentModel!} - showMode - showFeatures + <div className='mb-8'> + <ModelParameterModal + popupClassName='!w-[520px]' + portalToFollowElemContentClassName='z-[1000]' + isAdvancedMode={true} + provider={model.provider} + mode={model.mode} + completionParams={model.completion_params} + modelId={model.name} + setModel={handleModelChange} + onCompletionParamsChange={handleCompletionParamsChange} + hideDebugWithMultipleModel /> </div> <div > diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index ddae2f5b26..c0db0d7213 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import React from 'react' +import React, { useCallback, useEffect } from 'react' import cn from 'classnames' import useBoolean from 'ahooks/lib/useBoolean' import { useTranslation } from 'react-i18next' @@ -7,8 +7,10 @@ import ConfigPrompt from '../../config-prompt' import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index' import { generateRuleCode } from '@/service/debug' import type { CodeGenRes } from '@/service/debug' -import { type AppType, type Model, ModelModeType } from '@/types/app' +import type { ModelModeType } from '@/types/app' +import type { AppType, CompletionParams, Model } from '@/types/app' import Modal from '@/app/components/base/modal' +import Textarea from '@/app/components/base/textarea' import Button from '@/app/components/base/button' import { Generator } from '@/app/components/base/icons/src/vender/other' import Toast from '@/app/components/base/toast' @@ -17,8 +19,9 @@ import Confirm from '@/app/components/base/confirm' import type { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' -import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' +import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' +import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' + export type IGetCodeGeneratorResProps = { mode: AppType isShow: boolean @@ -36,11 +39,28 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( onFinished, }, ) => { + const { t } = useTranslation() + const defaultCompletionParams = { + temperature: 0.7, + max_tokens: 0, + top_p: 0, + echo: false, + stop: [], + presence_penalty: 0, + frequency_penalty: 0, + } + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model + : null + const [model, setModel] = React.useState<Model>(localModel || { + name: '', + provider: '', + mode: mode as unknown as ModelModeType.chat, + completion_params: defaultCompletionParams, + }) const { - currentProvider, - currentModel, + defaultModel, } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) - const { t } = useTranslation() const [instruction, setInstruction] = React.useState<string>('') const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) const [res, setRes] = React.useState<CodeGenRes | null>(null) @@ -56,21 +76,27 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( } return true } - const model: Model = { - provider: currentProvider?.provider || '', - name: currentModel?.model || '', - mode: ModelModeType.chat, - // This is a fixed parameter - completion_params: { - temperature: 0.7, - max_tokens: 0, - top_p: 0, - echo: false, - stop: [], - presence_penalty: 0, - frequency_penalty: 0, - }, - } + + const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => { + const newModel = { + ...model, + provider: newValue.provider, + name: newValue.modelId, + mode: newValue.mode as ModelModeType, + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) + + const handleCompletionParamsChange = useCallback((newParams: FormValue) => { + const newModel = { + ...model, + completion_params: newParams as CompletionParams, + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) + const isInLLMNode = true const onGenerate = async () => { if (!isValid()) @@ -99,16 +125,40 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( } const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false) + useEffect(() => { + if (defaultModel) { + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') || '') + : null + if (localModel) { + setModel({ + ...localModel, + completion_params: { + ...defaultCompletionParams, + ...localModel.completion_params, + }, + }) + } + else { + setModel(prev => ({ + ...prev, + name: defaultModel.model, + provider: defaultModel.provider.provider, + })) + } + } + }, [defaultModel]) + const renderLoading = ( <div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3'> <Loading /> - <div className='text-[13px] text-gray-400'>{t('appDebug.codegen.loading')}</div> + <div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div> </div> ) const renderNoData = ( <div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'> - <Generator className='h-14 w-14 text-gray-300' /> - <div className='text-center text-[13px] font-normal leading-5 text-gray-400'> + <Generator className='h-14 w-14 text-text-tertiary' /> + <div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'> <div>{t('appDebug.codegen.noDataLine1')}</div> <div>{t('appDebug.codegen.noDataLine2')}</div> </div> @@ -123,29 +173,30 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( closable > <div className='relative flex h-[680px] flex-wrap'> - <div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-gray-100 p-8'> + <div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-8'> <div className='mb-8'> - <div className={'text-lg font-bold leading-[28px]'}>{t('appDebug.codegen.title')}</div> - <div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.codegen.description')}</div> + <div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div> + <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div> </div> - <div className='flex items-center'> - <ModelIcon - className='mr-1.5 shrink-0' - provider={currentProvider} - modelName={currentModel?.model} - /> - <ModelName - className='grow' - modelItem={currentModel!} - showMode - showFeatures + <div className='mb-8'> + <ModelParameterModal + popupClassName='!w-[520px]' + portalToFollowElemContentClassName='z-[1000]' + isAdvancedMode={true} + provider={model.provider} + mode={model.mode} + completionParams={model.completion_params} + modelId={model.name} + setModel={handleModelChange} + onCompletionParamsChange={handleCompletionParamsChange} + hideDebugWithMultipleModel /> </div> - <div className='mt-6'> + <div> <div className='text-[0px]'> - <div className='mb-2 text-sm font-medium leading-5 text-gray-900'>{t('appDebug.codegen.instruction')}</div> - <textarea - className="h-[200px] w-full overflow-y-auto rounded-lg bg-gray-50 px-3 py-2 text-sm" + <div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.codegen.instruction')}</div> + <Textarea + className="h-[200px] resize-none" placeholder={t('appDebug.codegen.instructionPlaceholder') || ''} value={instruction} onChange={e => setInstruction(e.target.value)} @@ -169,7 +220,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( {!isLoading && !res && renderNoData} {(!isLoading && res) && ( <div className='h-full w-0 grow p-6 pb-0'> - <div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-gray-800'>{t('appDebug.codegen.resTitle')}</div> + <div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.codegen.resTitle')}</div> <div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}> <ConfigPrompt mode={mode} @@ -185,7 +236,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( <> {res?.code && ( <div className='mt-4'> - <h3 className='mb-2 text-sm font-medium text-gray-900'>{t('appDebug.codegen.generatedCode')}</h3> + <h3 className='mb-2 text-sm font-medium text-text-primary'>{t('appDebug.codegen.generatedCode')}</h3> <pre className='overflow-x-auto rounded-lg bg-gray-50 p-4'> <code className={`language-${res.language}`}> {res.code} @@ -202,7 +253,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( )} </div> - <div className='flex justify-end bg-white py-4'> + <div className='flex justify-end bg-background-default py-4'> <Button onClick={onClose}>{t('common.operation.cancel')}</Button> <Button variant='primary' className='ml-2' onClick={() => { setShowConfirmOverwrite(true) diff --git a/web/app/components/app/configuration/config/config-audio.tsx b/web/app/components/app/configuration/config/config-audio.tsx new file mode 100644 index 0000000000..5600f8cbb6 --- /dev/null +++ b/web/app/components/app/configuration/config/config-audio.tsx @@ -0,0 +1,78 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import produce from 'immer' +import { useContext } from 'use-context-selector' + +import { Microphone01 } from '@/app/components/base/icons/src/vender/features' +import Tooltip from '@/app/components/base/tooltip' +import ConfigContext from '@/context/debug-configuration' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' +import Switch from '@/app/components/base/switch' + +const ConfigAudio: FC = () => { + const { t } = useTranslation() + const file = useFeatures(s => s.features.file) + const featuresStore = useFeaturesStore() + const { isShowAudioConfig } = useContext(ConfigContext) + + const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false + + const handleChange = useCallback((value: boolean) => { + const { + features, + setFeatures, + } = featuresStore!.getState() + + const newFeatures = produce(features, (draft) => { + if (value) { + draft.file!.allowed_file_types = Array.from(new Set([ + ...(draft.file?.allowed_file_types || []), + SupportUploadFileTypes.audio, + ])) + } + else { + draft.file!.allowed_file_types = draft.file!.allowed_file_types?.filter( + type => type !== SupportUploadFileTypes.audio, + ) + } + if (draft.file) + draft.file.enabled = (draft.file.allowed_file_types?.length ?? 0) > 0 + }) + setFeatures(newFeatures) + }, [featuresStore]) + + if (!isShowAudioConfig) + return null + + return ( + <div className='mt-2 flex items-center gap-2 rounded-xl border-l-[0.5px] border-t-[0.5px] bg-background-section-burn p-2'> + <div className='shrink-0 p-1'> + <div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-600 p-1 shadow-xs'> + <Microphone01 className='h-4 w-4 text-text-primary-on-surface' /> + </div> + </div> + <div className='flex grow items-center'> + <div className='system-sm-semibold mr-1 text-text-secondary'>{t('appDebug.feature.audioUpload.title')}</div> + <Tooltip + popupContent={ + <div className='w-[180px]' > + {t('appDebug.feature.audioUpload.description')} + </div> + } + /> + </div> + <div className='flex shrink-0 items-center'> + <div className='ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle'></div> + <Switch + defaultValue={isAudioEnabled} + onChange={handleChange} + size='md' + /> + </div> + </div> + ) +} +export default React.memo(ConfigAudio) diff --git a/web/app/components/app/configuration/config/index.tsx b/web/app/components/app/configuration/config/index.tsx index dc2095502e..d0375c6de9 100644 --- a/web/app/components/app/configuration/config/index.tsx +++ b/web/app/components/app/configuration/config/index.tsx @@ -8,6 +8,7 @@ import DatasetConfig from '../dataset-config' import HistoryPanel from '../config-prompt/conversation-history/history-panel' import ConfigVision from '../config-vision' import ConfigDocument from './config-document' +import ConfigAudio from './config-audio' import AgentTools from './agent/agent-tools' import ConfigContext from '@/context/debug-configuration' import ConfigPrompt from '@/app/components/app/configuration/config-prompt' @@ -85,6 +86,8 @@ const Config: FC = () => { <ConfigDocument /> + <ConfigAudio /> + {/* Chat History */} {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( <HistoryPanel diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 3170d33a82..9835481ae0 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -31,6 +31,7 @@ import { import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fetchMembers } from '@/service/common' import type { Member } from '@/models/common' +import { useDocLink } from '@/context/i18n' type SettingsModalProps = { currentDataset: DataSet @@ -58,6 +59,7 @@ const SettingsModal: FC<SettingsModalProps> = ({ currentModel: isRerankDefaultModelValid, } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank) const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const ref = useRef(null) const isExternal = currentDataset.provider === 'external' @@ -328,7 +330,7 @@ const SettingsModal: FC<SettingsModalProps> = ({ <div> <div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='text-xs font-normal leading-[18px] text-text-tertiary'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting')} className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.description')} </div> </div> diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index 477328dad3..38b0c890e2 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -156,12 +156,11 @@ const Debug: FC<IDebug> = ({ } let hasEmptyInput = '' const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required, type }) => { - if (type !== 'string' && type !== 'paragraph' && type !== 'select') + if (type !== 'string' && type !== 'paragraph' && type !== 'select' && type !== 'number') return false const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) return res }) // compatible with old version - // debugger requiredVars.forEach(({ key, name }) => { if (hasEmptyInput) return diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2b97a64f5b..02f1928eca 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -80,6 +80,8 @@ import { import PluginDependency from '@/app/components/workflow/plugin-dependency' import { supportFunctionCall } from '@/utils/tool-call' import { MittProvider } from '@/context/mitt-context' +import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' +import Toast from '@/app/components/base/toast' type PublishConfig = { modelConfig: ModelConfig @@ -453,11 +455,26 @@ const Configuration: FC = () => { ...visionConfig, enabled: supportVision, }, true) - setCompletionParams({}) + + try { + const { params: filtered, removedDetails } = await fetchAndMergeValidCompletionParams( + provider, + modelId, + completionParams, + ) + if (Object.keys(removedDetails).length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` }) + setCompletionParams(filtered) + } + catch (e) { + Toast.notify({ type: 'error', message: t('common.error') }) + setCompletionParams({}) + } } const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document) + const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio) const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video) // *** web app features *** const featuresData: FeaturesData = useMemo(() => { @@ -904,6 +921,7 @@ const Configuration: FC = () => { setVisionConfig: handleSetVisionConfig, isAllowVideoUpload, isShowDocumentConfig, + isShowAudioConfig, rerankSettingModalOpen, setRerankSettingModalOpen, }} diff --git a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx index cca775c86e..f207cddd16 100644 --- a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx +++ b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { onReturnToSimpleMode: () => void } @@ -13,7 +11,7 @@ const AdvancedModeWarning: FC<Props> = ({ onReturnToSimpleMode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [show, setShow] = React.useState(true) if (!show) return null @@ -25,7 +23,7 @@ const AdvancedModeWarning: FC<Props> = ({ <span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span> <a className='font-medium text-[#155EEF]' - href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? '/guides/features/prompt-engineering' : 'features/prompt-engineering'}`} + href={docLink('/guides/features/prompt-engineering')} target='_blank' rel='noopener noreferrer' > {t('appDebug.promptMode.advancedWarning.learnMore')} diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index e509ee50e4..b36bf8848a 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -177,7 +177,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({ <div className='flex justify-between border-t border-divider-subtle p-4 pt-3'> <Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button> {canNotRun && ( - <Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')} needsDelay> + <Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')}> <Button variant="primary" disabled={canNotRun} diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index ee4bd57325..3fd020f60f 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -20,6 +20,7 @@ import type { import { useToastContext } from '@/app/components/base/toast' import AppIcon from '@/app/components/base/app-icon' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['api'] type ExternalDataToolModalProps = { @@ -40,6 +41,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({ onValidateBeforeSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) @@ -243,7 +245,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({ <div className='flex h-9 items-center justify-between text-sm font-medium text-gray-900'> {t('common.apiBasedExtension.selector.title')} <a - href={t('common.apiBasedExtension.linkUrl') || '/'} + href={docLink('/guides/extension/api-based-extension/README')} target='_blank' rel='noopener noreferrer' className='group flex items-center text-xs font-normal text-gray-500 hover:text-primary-600' > diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 46cb495801..f0a0da41a5 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -1,9 +1,9 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter, useSearchParams } from 'next/navigation' +import { useRouter } from 'next/navigation' import { useContext, useContextSelector } from 'use-context-selector' import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react' import Link from 'next/link' @@ -19,7 +19,6 @@ import AppsContext, { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' import type { AppMode } from '@/types/app' -import { AppModes } from '@/types/app' import { createApp } from '@/service/apps' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' @@ -30,6 +29,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' import FullScreenModal from '@/app/components/base/fullscreen-modal' import useTheme from '@/hooks/use-theme' +import { useDocLink } from '@/context/i18n' type CreateAppProps = { onSuccess: () => void @@ -56,14 +56,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) const isCreatingRef = useRef(false) - const searchParams = useSearchParams() - - useEffect(() => { - const category = searchParams.get('category') - if (category && AppModes.includes(category as AppMode)) - setAppMode(category as AppMode) - }, [searchParams]) - const onCreate = useCallback(async () => { if (!appMode) { notify({ type: 'error', message: t('app.newApp.appTypeRequired') }) @@ -128,7 +120,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) onClick={() => { setAppMode('workflow') }} /> - <AppTypeCard + <AppTypeCard active={appMode === 'advanced-chat'} title={t('app.types.advanced')} description={t('app.newApp.advancedShortDescription')} @@ -312,31 +304,41 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP function AppPreview({ mode }: { mode: AppMode }) { const { t } = useTranslation() + const docLink = useDocLink() const modeToPreviewInfoMap = { 'chat': { title: t('app.types.chatbot'), description: t('app.newApp.chatbotUserDescription'), - link: 'https://docs.dify.ai/guides/application-orchestrate/readme', + link: docLink('/guides/application-orchestrate/chatbot-application'), }, 'advanced-chat': { title: t('app.types.advanced'), description: t('app.newApp.advancedUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, 'agent-chat': { title: t('app.types.agent'), description: t('app.newApp.agentUserDescription'), - link: 'https://docs.dify.ai/en/guides/application-orchestrate/agent', + link: docLink('/guides/application-orchestrate/agent'), }, 'completion': { title: t('app.newApp.completeApp'), description: t('app.newApp.completionUserDescription'), - link: null, + link: docLink('/guides/application-orchestrate/text-generator', { + 'zh-Hans': '/guides/application-orchestrate/readme', + 'ja-JP': '/guides/application-orchestrate/README', + }), }, 'workflow': { title: t('app.types.workflow'), description: t('app.newApp.workflowUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/README', { + 'zh-Hans': '/guides/workflow/readme', + 'ja-JP': '/guides/workflow/concepts', + }), }, } const previewInfo = modeToPreviewInfoMap[mode] diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 9739ac47ea..8faafe05a8 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { MouseEventHandler } from 'react' -import { useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' @@ -35,6 +35,7 @@ type CreateFromDSLModalProps = { onClose: () => void activeTab?: string dslUrl?: string + droppedFile?: File } export enum CreateFromDSLModalTab { @@ -42,11 +43,11 @@ export enum CreateFromDSLModalTab { FROM_URL = 'from-url', } -const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => { +const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '', droppedFile }: CreateFromDSLModalProps) => { const { push } = useRouter() const { t } = useTranslation() const { notify } = useContext(ToastContext) - const [currentFile, setDSLFile] = useState<File>() + const [currentFile, setDSLFile] = useState<File | undefined>(droppedFile) const [fileContent, setFileContent] = useState<string>() const [currentTab, setCurrentTab] = useState(activeTab) const [dslUrlValue, setDslUrlValue] = useState(dslUrl) @@ -78,6 +79,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS const isCreatingRef = useRef(false) + useEffect(() => { + if (droppedFile) + handleFile(droppedFile) + }, [droppedFile]) + const onCreate: MouseEventHandler = async () => { if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) return diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 3062e3a911..b04148d484 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -41,6 +41,7 @@ import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import cn from '@/utils/classnames' import { noop } from 'lodash-es' +import PromptLogModal from '../../base/prompt-log-modal' dayjs.extend(utc) dayjs.extend(timezone) @@ -190,11 +191,14 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const { userProfile: { timezone } } = useAppContext() const { formatTime } = useTimestamp() const { onClose, appDetail } = useContext(DrawerContext) - const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ + const { notify } = useContext(ToastContext) + const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, showMessageLogModal: state.showMessageLogModal, setShowMessageLogModal: state.setShowMessageLogModal, + showPromptLogModal: state.showPromptLogModal, + setShowPromptLogModal: state.setShowPromptLogModal, currentLogModalActiveTab: state.currentLogModalActiveTab, }))) const { t } = useTranslation() @@ -309,18 +313,34 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { return item })) }, [allChatItems]) - const handleAnnotationRemoved = useCallback((index: number) => { - setAllChatItems(allChatItems.map((item, i) => { - if (i === index) { - return { - ...item, - content: item.content, - annotation: undefined, - } + const handleAnnotationRemoved = useCallback(async (index: number): Promise<boolean> => { + const annotation = allChatItems[index]?.annotation + + try { + if (annotation?.id) { + const { delAnnotation } = await import('@/service/annotation') + await delAnnotation(appDetail?.id || '', annotation.id) } - return item - })) - }, [allChatItems]) + + setAllChatItems(allChatItems.map((item, i) => { + if (i === index) { + return { + ...item, + content: item.content, + annotation: undefined, + } + } + return item + })) + + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + return true + } + catch { + notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + return false + } + }, [allChatItems, appDetail?.id, t]) const fetchInitiated = useRef(false) @@ -354,7 +374,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { } useEffect(() => { - adjustModalWidth() + const raf = requestAnimationFrame(adjustModalWidth) + return () => cancelAnimationFrame(raf) }, []) return ( @@ -449,8 +470,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { className="py-4" id="scrollableDiv" style={{ - height: 1000, // Specify a value - overflow: 'auto', display: 'flex', flexDirection: 'column-reverse', }}> @@ -515,6 +534,16 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { defaultTab={currentLogModalActiveTab} /> )} + {!isChatMode && showPromptLogModal && ( + <PromptLogModal + width={width} + currentLogItem={currentLogItem} + onCancel={() => { + setCurrentLogItem() + setShowPromptLogModal(false) + }} + /> + )} </div> ) } diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 9f3b3ac4a6..f11e111cb0 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -181,6 +181,7 @@ function AppCard({ icon={appInfo.icon} icon_background={appInfo.icon_background} name={basicName} + hideType type={ isApp ? t('appOverview.overview.appInfo.explanation') diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index 4e84dd8b1f..0fedd76f89 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -3,13 +3,11 @@ import type { FC } from 'react' import React from 'react' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useDocLink } from '@/context/i18n' import type { AppMode } from '@/types/app' -import I18n from '@/context/i18n' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' import Tag from '@/app/components/base/tag' -import { LanguagesSupported } from '@/i18n/language' type IShareLinkProps = { isShow: boolean @@ -43,7 +41,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({ mode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const isChatApp = mode === 'chat' || mode === 'advanced-chat' return <Modal @@ -101,10 +99,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({ className='mt-2' onClick={() => window.open( - `https://docs.dify.ai/${locale !== LanguagesSupported[1] - ? 'user-guide/launching-dify-apps/developing-with-apis' - : `${locale.toLowerCase()}/guides/application-publishing/developing-with-apis` - }`, + docLink('/guides/application-publishing/developing-with-apis'), '_blank', ) } diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 691b727b8e..b48eac5458 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -50,6 +50,10 @@ const OPTION_MAP = { // user_id: 'YOU CAN DEFINE USER ID HERE', // conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID', }, + userVariables: { + // avatar_url: 'YOU CAN DEFINE USER AVATAR URL HERE', + // name: 'YOU CAN DEFINE USER NAME HERE', + }, } </script> <script diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 98e2cab681..524c340a53 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react' import Link from 'next/link' import { Trans, useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import Modal from '@/app/components/base/modal' import ActionButton from '@/app/components/base/action-button' @@ -19,14 +18,14 @@ import { SimpleSelect } from '@/app/components/base/select' import type { AppDetailResponse } from '@/models/app' import type { AppIconType, AppSSO, Language } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' -import { LanguagesSupported, languages } from '@/i18n/language' +import { languages } from '@/i18n/language' import Tooltip from '@/app/components/base/tooltip' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker' -import I18n from '@/context/i18n' import cn from '@/utils/classnames' +import { useDocLink } from '@/context/i18n' export type ISettingsModalProps = { isChat: boolean @@ -98,7 +97,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({ const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [appIcon, setAppIcon] = useState<AppIconSelection>( @@ -238,7 +237,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({ </div> <div className='system-xs-regular mt-0.5 text-text-tertiary'> <span>{t(`${prefixSettings}.modalTip`)}</span> - <Link href={`${locale === LanguagesSupported[1] ? 'https://docs.dify.ai/zh-hans/guides/application-publishing/launch-your-webapp-quickly#she-zhi-ni-de-ai-zhan-dian' : 'https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README'}`} target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link> + <Link href={docLink('/guides/application-publishing/launch-your-webapp-quickly/README', { + 'zh-Hans': '/guides/application-publishing/launch-your-webapp-quickly/readme', + })} + target='_blank' rel='noopener noreferrer' className='text-text-accent'>{t('common.operation.learnMore')}</Link> </div> </div> {/* form body */} diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index aa3ffa33c4..92d86351e0 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -171,7 +171,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({ appId: params.appId as string, messageId: messageId!, }) - const logItem = { + const logItem = Array.isArray(data.message) ? { ...data, log: [ ...data.message, @@ -185,6 +185,11 @@ const GenerationItem: FC<IGenerationItemProps> = ({ ] : []), ], + } : { + ...data, + log: [typeof data.message === 'string' ? { + text: data.message, + } : data.message], } setCurrentLogItem(logItem) setShowPromptLogModal(true) diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index 0accafdf4b..a57bac20db 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -15,7 +15,7 @@ export type AppSelectorProps = { onChange: (value: AppSelectorProps['value']) => void } -const allTypes: AppMode[] = ['chat', 'agent-chat', 'completion', 'advanced-chat', 'workflow'] +const allTypes: AppMode[] = ['workflow', 'advanced-chat', 'chat', 'agent-chat', 'completion'] const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => { const [open, setOpen] = useState(false) diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index 975f8aeb6c..8e66cd38cf 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -112,7 +112,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({ isShow closable={false} wrapperClassName={className} - className={cn(s.container, '!w-[362px] !p-0')} + className={cn(s.container, '!h-[462px] !w-[362px] !p-0')} > {!DISABLE_UPLOAD_IMAGE_AS_ICON && <div className="w-full p-2 pb-0"> <div className='flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary'> @@ -131,8 +131,8 @@ const AppIconPicker: FC<AppIconPickerProps> = ({ </div> </div>} - <EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} /> - <ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} /> + {activeTab === 'emoji' && <EmojiPickerInner className={cn('flex-1 overflow-hidden pt-2')} onSelect={handleSelectEmoji} />} + {activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />} <Divider className='m-0' /> <div className='flex w-full items-center justify-center gap-2 p-3'> diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index ac17af1988..003d929c8c 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -18,6 +18,7 @@ export type AppIconProps = { imageUrl?: string | null className?: string innerIcon?: React.ReactNode + coverElement?: React.ReactNode onClick?: () => void } const appIconVariants = cva( @@ -51,6 +52,7 @@ const AppIcon: FC<AppIconProps> = ({ imageUrl, className, innerIcon, + coverElement, onClick, }) => { const isValidImageIcon = iconType === 'image' && imageUrl @@ -65,6 +67,7 @@ const AppIcon: FC<AppIconProps> = ({ ? <img src={imageUrl} className="h-full w-full" alt="app icon" /> : (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)) } + {coverElement} </span> } diff --git a/web/app/components/base/app-unavailable.tsx b/web/app/components/base/app-unavailable.tsx index 928c850262..c501d36118 100644 --- a/web/app/components/base/app-unavailable.tsx +++ b/web/app/components/base/app-unavailable.tsx @@ -21,7 +21,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({ return ( <div className={classNames('flex h-screen w-screen items-center justify-center', className)}> - <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' + <h1 className='mr-5 h-[50px] shrink-0 pr-5 text-[24px] font-medium leading-[50px]' style={{ borderRight: '1px solid rgba(0,0,0,.3)', }}>{code}</h1> diff --git a/web/app/components/base/button/sync-button.tsx b/web/app/components/base/button/sync-button.tsx new file mode 100644 index 0000000000..013c86889a --- /dev/null +++ b/web/app/components/base/button/sync-button.tsx @@ -0,0 +1,27 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { RiRefreshLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import TooltipPlus from '@/app/components/base/tooltip' + +type Props = { + className?: string, + popupContent?: string, + onClick: () => void +} + +const SyncButton: FC<Props> = ({ + className, + popupContent = '', + onClick, +}) => { + return ( + <TooltipPlus popupContent={popupContent}> + <div className={cn(className, 'cursor-pointer select-none rounded-md p-1 hover:bg-state-base-hover')} onClick={onClick}> + <RiRefreshLine className='h-4 w-4 text-text-tertiary' /> + </div> + </TooltipPlus> + ) +} +export default React.memo(SyncButton) diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index 1fd1383196..fe8e7b430d 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,5 +1,7 @@ +'use client' import type { FC } from 'react' import { + useCallback, useEffect, useState, } from 'react' @@ -17,10 +19,12 @@ import ChatWrapper from './chat-wrapper' import type { InstalledApp } from '@/models/explore' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { checkOrSetAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' +import { useTranslation } from 'react-i18next' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' type ChatWithHistoryProps = { className?: string @@ -38,6 +42,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ isMobile, themeBuilder, sidebarCollapseState, + isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -51,13 +56,34 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({ useDocumentTitle(site?.title || 'Chat') + const { t } = useTranslation() + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (appInfoLoading) { return ( <Loading type='app' /> ) } - if (!userCanAccess) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (!userCanAccess) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } if (appInfoError) { return ( diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx index 52490e4024..cbfa3168e9 100644 --- a/web/app/components/base/chat/chat/chat-input-area/index.tsx +++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx @@ -192,7 +192,7 @@ const ChatInputArea = ({ <Textarea ref={ref => textareaRef.current = ref as any} className={cn( - 'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-tertiary outline-none', + 'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none', )} placeholder={t('common.chat.inputPlaceholder', { botName }) || ''} autoFocus diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 10fb455d33..17373cec9d 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -83,12 +83,11 @@ export const useChat = ( const ret = [...threadMessages] if (config?.opening_statement) { const index = threadMessages.findIndex(item => item.isOpeningStatement) - if (index > -1) { ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), } } else { @@ -97,7 +96,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), }) } } diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index c0842af0c4..16717c53f9 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -274,7 +274,7 @@ const Chat: FC<ChatProps> = ({ </div> </div> <div - className={`absolute bottom-0 flex justify-center bg-chat-input-mask ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`} + className={`absolute bottom-0 z-10 flex justify-center bg-chat-input-mask ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`} ref={chatFooterRef} > <div @@ -303,7 +303,7 @@ const Chat: FC<ChatProps> = ({ { !noChatInput && ( <ChatInputArea - botName={appData?.site.title || ''} + botName={appData?.site.title || 'Bot'} disabled={inputDisabled} showFeatureBar={showFeatureBar} showFileUpload={showFileUpload} diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx index 30077125f9..6630d9bb9d 100644 --- a/web/app/components/base/chat/chat/question.tsx +++ b/web/app/components/base/chat/chat/question.tsx @@ -98,7 +98,7 @@ const Question: FC<QuestionProps> = ({ return ( <div className='mb-2 flex justify-end last:mb-0'> - <div className={cn('group relative mr-4 flex max-w-full items-start pl-14', isEditing && 'flex-1')}> + <div className={cn('group relative mr-4 flex max-w-full items-start overflow-x-hidden pl-14', isEditing && 'flex-1')}> <div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}> <div className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex" @@ -130,7 +130,7 @@ const Question: FC<QuestionProps> = ({ /> ) } - { !isEditing + {!isEditing ? <Markdown content={content} /> : <div className=" flex flex-col gap-2 rounded-xl @@ -151,8 +151,8 @@ const Question: FC<QuestionProps> = ({ <Button variant='ghost' onClick={handleCancelEditing}>{t('common.operation.cancel')}</Button> <Button variant='primary' onClick={handleResend}>{t('common.chat.resend')}</Button> </div> - </div> } - { !isEditing && <ContentSwitch + </div>} + {!isEditing && <ContentSwitch count={item.siblingCount} currentIndex={item.siblingIndex} prevDisabled={!item.prevSibling} diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index d81d8af2f8..8429c82e07 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -25,6 +25,7 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested import { Markdown } from '@/app/components/base/markdown' import cn from '@/utils/classnames' import type { FileEntity } from '../../file-uploader/types' +import Avatar from '../../avatar' const ChatWrapper = () => { const { @@ -49,6 +50,7 @@ const ChatWrapper = () => { setClearChatList, setIsResponding, allInputsHidden, + initUserVariables, } = useEmbeddedChatbotContext() const appConfig = useMemo(() => { const config = appParams || {} @@ -261,6 +263,14 @@ const ChatWrapper = () => { switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} inputDisabled={inputDisabled} isMobile={isMobile} + questionIcon={ + initUserVariables?.avatar_url + ? <Avatar + avatar={initUserVariables.avatar_url} + name={initUserVariables.name || 'user'} + size={40} + /> : undefined + } /> ) } diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index d24265ed9e..544da253af 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -52,6 +52,10 @@ export type EmbeddedChatbotContextValue = { currentConversationInputs: Record<string, any> | null, setCurrentConversationInputs: (v: Record<string, any>) => void, allInputsHidden: boolean + initUserVariables?: { + name?: string + avatar_url?: string + } } export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ @@ -81,5 +85,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue> currentConversationInputs: {}, setCurrentConversationInputs: noop, allInputsHidden: false, + initUserVariables: {}, }) export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 0158e8d041..7dd665efdc 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -15,7 +15,7 @@ import type { Feedback, } from '../types' import { CONVERSATION_ID_INFO } from '../constants' -import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams } from '../utils' +import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils' import { getProcessedFilesFromResponse } from '../../file-uploader/utils' import { fetchAppInfo, @@ -169,6 +169,7 @@ export const useEmbeddedChatbot = () => { const newConversationInputsRef = useRef<Record<string, any>>({}) const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({}) const [initInputs, setInitInputs] = useState<Record<string, any>>({}) + const [initUserVariables, setInitUserVariables] = useState<Record<string, any>>({}) const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => { newConversationInputsRef.current = newInputs setNewConversationInputs(newInputs) @@ -237,7 +238,9 @@ export const useEmbeddedChatbot = () => { // init inputs from url params (async () => { const inputs = await getProcessedInputsFromUrlParams() + const userVariables = await getProcessedUserVariablesFromUrlParams() setInitInputs(inputs) + setInitUserVariables(userVariables) })() }, []) useEffect(() => { @@ -418,5 +421,6 @@ export const useEmbeddedChatbot = () => { currentConversationInputs, setCurrentConversationInputs, allInputsHidden, + initUserVariables, } } diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx index 002d142542..69cf7f163b 100644 --- a/web/app/components/base/chat/embedded-chatbot/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/index.tsx @@ -1,4 +1,6 @@ +'use client' import { + useCallback, useEffect, useState, } from 'react' @@ -12,7 +14,7 @@ import { useEmbeddedChatbot } from './hooks' import { isDify } from './utils' import { useThemeContext } from './theme/theme-context' import { CssTransform } from './theme/utils' -import { checkOrSetAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Loading from '@/app/components/base/loading' @@ -23,6 +25,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' import { useGlobalPublicStore } from '@/context/global-public-context' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' const Chatbot = () => { const { @@ -36,6 +39,7 @@ const Chatbot = () => { chatShouldReloadKey, handleNewConversation, themeBuilder, + isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -51,6 +55,22 @@ const Chatbot = () => { useDocumentTitle(site?.title || 'Chat') + const searchParams = useSearchParams() + const router = useRouter() + const pathname = usePathname() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (appInfoLoading) { return ( <> @@ -66,8 +86,12 @@ const Chatbot = () => { ) } - if (!userCanAccess) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (!userCanAccess) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } if (appInfoError) { return ( @@ -141,7 +165,6 @@ const EmbeddedChatbotWrapper = () => { appInfoError, appInfoLoading, appData, - accessMode, userCanAccess, appParams, appMeta, @@ -172,11 +195,11 @@ const EmbeddedChatbotWrapper = () => { currentConversationInputs, setCurrentConversationInputs, allInputsHidden, + initUserVariables, } = useEmbeddedChatbot() return <EmbeddedChatbotContext.Provider value={{ userCanAccess, - accessMode, appInfoError, appInfoLoading, appData, @@ -211,6 +234,7 @@ const EmbeddedChatbotWrapper = () => { currentConversationInputs, setCurrentConversationInputs, allInputsHidden, + initUserVariables, }}> <Chatbot /> </EmbeddedChatbotContext.Provider> diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts index 8e044375d3..fb7ac93a4b 100644 --- a/web/app/components/base/chat/utils.ts +++ b/web/app/components/base/chat/utils.ts @@ -32,7 +32,8 @@ async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> { const entriesArray = Array.from(urlParams.entries()) await Promise.all( entriesArray.map(async ([key, value]) => { - if (!key.startsWith('sys.')) + const prefixArray = ['sys.', 'user.'] + if (!prefixArray.some(prefix => key.startsWith(prefix))) inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value)) }), ) @@ -52,6 +53,19 @@ async function getProcessedSystemVariablesFromUrlParams(): Promise<Record<string return systemVariables } +async function getProcessedUserVariablesFromUrlParams(): Promise<Record<string, any>> { + const urlParams = new URLSearchParams(window.location.search) + const userVariables: Record<string, any> = {} + const entriesArray = Array.from(urlParams.entries()) + await Promise.all( + entriesArray.map(async ([key, value]) => { + if (key.startsWith('user.')) + userVariables[key.slice(5)] = await decodeBase64AndDecompress(decodeURIComponent(value)) + }), + ) + return userVariables +} + function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean { return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement } @@ -198,6 +212,7 @@ export { getRawInputsFromUrlParams, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, + getProcessedUserVariablesFromUrlParams, isValidGeneratedAnswer, getLastAnswer, buildChatItemTree, diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 34ce3f7da0..6fc4b67181 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -5,6 +5,8 @@ import data from '@emoji-mart/data' import type { EmojiMartData } from '@emoji-mart/data' import { init } from 'emoji-mart' import { + ChevronDownIcon, + ChevronUpIcon, MagnifyingGlassIcon, } from '@heroicons/react/24/outline' import Input from '@/app/components/base/input' @@ -60,16 +62,20 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({ const { categories } = data as EmojiMartData const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) + const [showStyleColors, setShowStyleColors] = useState(false) const [searchedEmojis, setSearchedEmojis] = useState<string[]>([]) const [isSearching, setIsSearching] = useState(false) React.useEffect(() => { - if (selectedEmoji && selectedBackground) - onSelect?.(selectedEmoji, selectedBackground) + if (selectedEmoji) { + setShowStyleColors(true) + if (selectedBackground) + onSelect?.(selectedEmoji, selectedBackground) + } }, [onSelect, selectedEmoji, selectedBackground]) - return <div className={cn(className)}> + return <div className={cn(className, 'flex flex-col')}> <div className='flex w-full flex-col items-center px-3 pb-2'> <div className="relative w-full"> <div className="pointer-events-none absolute inset-y-0 left-0 z-10 flex items-center pl-3"> @@ -141,33 +147,34 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({ </div> {/* Color Select */} - <div className={cn('p-3 pb-0', selectedEmoji === '' ? 'opacity-25' : '')}> + <div className={cn('flex items-center justify-between p-3 pb-0')}> <p className='system-xs-medium-uppercase mb-2 text-text-primary'>Choose Style</p> - <div className='grid h-full w-full grid-cols-8 gap-1'> - {backgroundColors.map((color) => { - return <div - key={color} - className={ - cn( - 'cursor-pointer', - 'ring-offset-1 hover:ring-1', - 'inline-flex h-10 w-10 items-center justify-center rounded-lg', - color === selectedBackground ? 'ring-1 ring-components-input-border-hover' : '', - )} - onClick={() => { - setSelectedBackground(color) - }} - > - <div className={cn( - 'flex h-8 w-8 items-center justify-center rounded-lg p-1', - ) - } style={{ background: color }}> - {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />} - </div> - </div> - })} - </div> + {showStyleColors ? <ChevronDownIcon className='h-4 w-4' onClick={() => setShowStyleColors(!showStyleColors)} /> : <ChevronUpIcon className='h-4 w-4' onClick={() => setShowStyleColors(!showStyleColors)} />} </div> + {showStyleColors && <div className='grid w-full grid-cols-8 gap-1 px-3'> + {backgroundColors.map((color) => { + return <div + key={color} + className={ + cn( + 'cursor-pointer', + 'ring-offset-1 hover:ring-1', + 'inline-flex h-10 w-10 items-center justify-center rounded-lg', + color === selectedBackground ? 'ring-1 ring-components-input-border-hover' : '', + )} + onClick={() => { + setSelectedBackground(color) + }} + > + <div className={cn( + 'flex h-8 w-8 items-center justify-center rounded-lg p-1', + ) + } style={{ background: color }}> + {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />} + </div> + </div> + })} + </div>} </div> } export default EmojiPickerInner diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index 117c8a5558..51e33c43d2 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -130,6 +130,7 @@ const OpeningSettingModal = ({ <input type="input" value={question || ''} + placeholder={t('appDebug.openingStatement.openingQuestionPlaceholder') as string} onChange={(e) => { const value = e.target.value setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => { diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index eee680596b..d00cd9038a 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { RiCloseLine, RiInformation2Fill } from '@remixicon/react' import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -19,8 +18,7 @@ import Moderation from '@/app/components/base/features/new-feature-panel/moderat import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply' import type { PromptVariable } from '@/models/debug' import type { InputVar } from '@/app/components/workflow/types' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { show: boolean @@ -48,7 +46,7 @@ const NewFeaturePanel = ({ onAutoAddPromptVariable, }: Props) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts) @@ -80,7 +78,7 @@ const NewFeaturePanel = ({ <span>{isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')}</span> <a className='text-text-accent' - href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}guides/workflow/bulletin`} + href={docLink('/guides/workflow/bulletin')} target='_blank' rel='noopener noreferrer' >{t('workflow.common.featuresDocLink')}</a> </div> diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index ab5200f38f..3ede2d7c6b 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -25,6 +25,7 @@ import { useModalContext } from '@/context/modal-context' import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import cn from '@/utils/classnames' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const systemTypes = ['openai_moderation', 'keywords', 'api'] @@ -46,6 +47,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({ onSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const { locale } = useContext(I18n) const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders) @@ -316,7 +318,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({ <div className='flex h-9 items-center justify-between'> <div className='text-sm font-medium text-text-primary'>{t('common.apiBasedExtension.selector.title')}</div> <a - href={t('common.apiBasedExtension.linkUrl') || '/'} + href={docLink('/guides/extension/api-based-extension/README')} target='_blank' rel='noopener noreferrer' className='group flex items-center text-xs text-text-tertiary hover:text-primary-600' > diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx index ab4e2aaa42..02bb3ad673 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx @@ -26,9 +26,11 @@ type Option = { icon: React.JSX.Element } type FileUploaderInAttachmentProps = { + isDisabled?: boolean fileConfig: FileUpload } const FileUploaderInAttachment = ({ + isDisabled, fileConfig, }: FileUploaderInAttachmentProps) => { const { t } = useTranslation() @@ -89,16 +91,18 @@ const FileUploaderInAttachment = ({ return ( <div> - <div className='flex items-center space-x-1'> - {options.map(renderOption)} - </div> + {!isDisabled && ( + <div className='flex items-center space-x-1'> + {options.map(renderOption)} + </div> + )} <div className='mt-1 space-y-1'> { files.map(file => ( <FileItem key={file.id} file={file} - showDeleteAction + showDeleteAction={!isDisabled} showDownloadAction={false} onRemove={() => handleRemoveFile(file.id)} onReUpload={() => handleReUploadFile(file.id)} @@ -114,18 +118,20 @@ type FileUploaderInAttachmentWrapperProps = { value?: FileEntity[] onChange: (files: FileEntity[]) => void fileConfig: FileUpload + isDisabled?: boolean } const FileUploaderInAttachmentWrapper = ({ value, onChange, fileConfig, + isDisabled, }: FileUploaderInAttachmentWrapperProps) => { return ( <FileContextProvider value={value} onChange={onChange} > - <FileUploaderInAttachment fileConfig={fileConfig} /> + <FileUploaderInAttachment isDisabled={isDisabled} fileConfig={fileConfig} /> </FileContextProvider> ) } diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts index 9b5a449481..e870f9edab 100644 --- a/web/app/components/base/file-uploader/utils.ts +++ b/web/app/components/base/file-uploader/utils.ts @@ -154,7 +154,7 @@ export const getProcessedFilesFromResponse = (files: FileResponse[]) => { transferMethod: fileItem.transfer_method, supportFileType: fileItem.type, uploadedId: fileItem.upload_file_id || fileItem.related_id, - url: fileItem.url, + url: fileItem.url || fileItem.remote_url, } }) } diff --git a/web/app/components/base/icons/assets/public/llm/openai-teal.svg b/web/app/components/base/icons/assets/public/llm/openai-teal.svg new file mode 100644 index 0000000000..359cb532b6 --- /dev/null +++ b/web/app/components/base/icons/assets/public/llm/openai-teal.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="24" height="24" rx="6" fill="#009688"/> +<path d="M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z" fill="white"/> +</svg> diff --git a/web/app/components/base/icons/assets/public/llm/openai-yellow.svg b/web/app/components/base/icons/assets/public/llm/openai-yellow.svg new file mode 100644 index 0000000000..015eb74adc --- /dev/null +++ b/web/app/components/base/icons/assets/public/llm/openai-yellow.svg @@ -0,0 +1,4 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect width="24" height="24" rx="6" fill="#FAB005"/> +<path d="M19.7758 11.5959C19.9546 11.9948 20.0681 12.4213 20.1145 12.8563C20.1592 13.2913 20.1369 13.7315 20.044 14.1596C19.9529 14.5878 19.7947 14.9987 19.5746 15.377C19.4302 15.6298 19.2599 15.867 19.0639 16.0854C18.8696 16.3021 18.653 16.4981 18.4174 16.67C18.1801 16.842 17.9274 16.9864 17.6591 17.105C17.3926 17.222 17.1141 17.3114 16.8286 17.3698C16.6945 17.7859 16.4951 18.1797 16.2371 18.5339C15.9809 18.8881 15.6697 19.1993 15.3155 19.4555C14.9613 19.7134 14.5693 19.9129 14.1532 20.047C13.7371 20.1829 13.302 20.2499 12.8636 20.2499C12.573 20.2516 12.2807 20.2207 11.9953 20.1622C11.7116 20.102 11.433 20.0109 11.1665 19.8923C10.9 19.7736 10.6472 19.6258 10.4116 19.4538C10.1778 19.2819 9.96115 19.0841 9.76857 18.8658C9.33871 18.9586 8.89853 18.981 8.46351 18.9363C8.02849 18.8898 7.60207 18.7763 7.20143 18.5975C6.80252 18.4204 6.43284 18.1797 6.10786 17.8857C5.78289 17.5916 5.50606 17.2478 5.28769 16.8695C5.14153 16.6167 5.02117 16.3502 4.93004 16.0734C4.83891 15.7965 4.77873 15.5111 4.74778 15.2205C4.71683 14.9317 4.71855 14.6393 4.7495 14.3488C4.78045 14.0599 4.84407 13.7745 4.9352 13.4976C4.64289 13.1727 4.40217 12.803 4.22335 12.4041C4.04624 12.0034 3.93104 11.5787 3.88634 11.1437C3.83991 10.7087 3.86398 10.2685 3.95511 9.84036C4.04624 9.41222 4.20443 9.00127 4.42452 8.62299C4.56896 8.37023 4.73918 8.13123 4.93348 7.91458C5.12778 7.69793 5.34615 7.50191 5.58171 7.32997C5.81728 7.15802 6.07176 7.01187 6.33827 6.89495C6.6065 6.7763 6.88506 6.68861 7.17048 6.63015C7.3046 6.21232 7.50406 5.82029 7.76026 5.46608C8.01817 5.11188 8.32939 4.80066 8.6836 4.54274C9.03781 4.28654 9.42984 4.08708 9.84595 3.95125C10.2621 3.81713 10.6971 3.74835 11.1355 3.75007C11.4261 3.74835 11.7184 3.77758 12.0039 3.83776C12.2893 3.89794 12.5678 3.98736 12.8344 4.106C13.1009 4.22636 13.3536 4.37251 13.5892 4.54446C13.8248 4.71812 14.0414 4.91414 14.234 5.13251C14.6621 5.04138 15.1023 5.01903 15.5373 5.06373C15.9723 5.10844 16.3971 5.22364 16.7977 5.40074C17.1966 5.57957 17.5663 5.81857 17.8913 6.1126C18.2162 6.4049 18.4931 6.74707 18.7114 7.12707C18.8576 7.37811 18.9779 7.64463 19.0691 7.92318C19.1602 8.20001 19.2221 8.48544 19.2513 8.77602C19.2823 9.06661 19.2823 9.35892 19.2496 9.64951C19.2187 9.94009 19.155 10.2255 19.0639 10.5024C19.3579 10.8273 19.5969 11.1953 19.7758 11.5959ZM14.0466 18.9363C14.4214 18.7815 14.7619 18.5528 15.049 18.2657C15.3362 17.9785 15.5648 17.6381 15.7196 17.2615C15.8743 16.8867 15.9552 16.4843 15.9552 16.0785V12.2442C15.954 12.2407 15.9529 12.2367 15.9517 12.2321C15.9506 12.2287 15.9488 12.2252 15.9466 12.2218C15.9443 12.2184 15.9414 12.2155 15.938 12.2132C15.9345 12.2098 15.9311 12.2075 15.9276 12.2063L14.54 11.4051V16.0373C14.54 16.0837 14.5332 16.1318 14.5211 16.1765C14.5091 16.223 14.4919 16.2659 14.4678 16.3072C14.4438 16.3485 14.4162 16.3863 14.3819 16.419C14.3484 16.4523 14.3109 16.4812 14.2701 16.505L10.9842 18.4015C10.9567 18.4187 10.9103 18.4428 10.8862 18.4565C11.0221 18.5717 11.1699 18.6732 11.3247 18.7626C11.4811 18.852 11.6428 18.9277 11.8113 18.9896C11.9798 19.0497 12.1535 19.0962 12.3288 19.1271C12.5059 19.1581 12.6848 19.1735 12.8636 19.1735C13.2694 19.1735 13.6717 19.0927 14.0466 18.9363ZM6.22135 16.333C6.42596 16.6855 6.69592 16.9916 7.01745 17.2392C7.34071 17.4868 7.70695 17.6673 8.09899 17.7722C8.49102 17.8771 8.90025 17.9046 9.3026 17.8513C9.70495 17.798 10.0918 17.6673 10.4443 17.4644L13.7663 15.5472L13.7749 15.5386C13.7772 15.5363 13.7789 15.5329 13.78 15.5283C13.7823 15.5249 13.7841 15.5214 13.7852 15.518V13.9017L9.77545 16.2212C9.73418 16.2453 9.6912 16.2625 9.64649 16.2763C9.60007 16.2883 9.55364 16.2935 9.5055 16.2935C9.45907 16.2935 9.41265 16.2883 9.36622 16.2763C9.32152 16.2625 9.27681 16.2453 9.23554 16.2212L5.94967 14.323C5.92044 14.3058 5.87746 14.28 5.85339 14.2645C5.82244 14.4416 5.80696 14.6204 5.80696 14.7993C5.80696 14.9781 5.82415 15.1569 5.85511 15.334C5.88605 15.5094 5.9342 15.6831 5.99438 15.8516C6.05628 16.0201 6.13194 16.1817 6.22135 16.3364V16.333ZM5.35818 9.1629C5.15529 9.51539 5.02461 9.90398 4.97131 10.3063C4.918 10.7087 4.94552 11.1162 5.0504 11.51C5.15529 11.902 5.33583 12.2682 5.58343 12.5915C5.83103 12.913 6.13881 13.183 6.48958 13.3859L9.80984 15.3048C9.81328 15.3059 9.81729 15.3071 9.82188 15.3082H9.83391C9.8385 15.3082 9.84251 15.3071 9.84595 15.3048C9.84939 15.3036 9.85283 15.3019 9.85627 15.2996L11.249 14.4949L7.23926 12.1805C7.19971 12.1565 7.16189 12.1272 7.1275 12.0946C7.09418 12.0611 7.06529 12.0236 7.04153 11.9828C7.01917 11.9415 7.00026 11.8985 6.98822 11.8521C6.97619 11.8074 6.96931 11.761 6.97103 11.7128V7.80797C6.80252 7.86987 6.63917 7.94553 6.48442 8.03494C6.32967 8.12607 6.18352 8.22924 6.04596 8.34444C5.91013 8.45965 5.78289 8.58688 5.66769 8.72444C5.55248 8.86028 5.45103 9.00815 5.36162 9.1629H5.35818ZM16.7633 11.8177C16.8046 11.8418 16.8424 11.8693 16.8768 11.9037C16.9094 11.9364 16.9387 11.9742 16.9628 12.0155C16.9851 12.0567 17.004 12.1014 17.0161 12.1461C17.0264 12.1926 17.0332 12.239 17.0315 12.2871V16.192C17.5835 15.9891 18.0649 15.6332 18.4208 15.1655C18.7785 14.6978 18.9934 14.139 19.0433 13.5544C19.0931 12.9698 18.9762 12.3817 18.7046 11.8607C18.4329 11.3397 18.0185 10.9064 17.5095 10.6141L14.1893 8.69521C14.1858 8.69406 14.1818 8.69292 14.1772 8.69177H14.1652C14.1618 8.69292 14.1578 8.69406 14.1532 8.69521C14.1497 8.69636 14.1463 8.69808 14.1429 8.70037L12.757 9.50163L16.7667 11.8177H16.7633ZM18.1475 9.7372H18.1457V9.73892L18.1475 9.7372ZM18.1457 9.73548C18.2455 9.15774 18.1784 8.56281 17.9514 8.02119C17.7262 7.47956 17.3496 7.01359 16.8682 6.67658C16.3867 6.34128 15.8193 6.1487 15.233 6.12291C14.6449 6.09884 14.0638 6.24155 13.5548 6.53386L10.2345 8.45105C10.2311 8.45334 10.2282 8.45621 10.2259 8.45965L10.2191 8.46996C10.2179 8.4734 10.2168 8.47741 10.2156 8.482C10.2145 8.48544 10.2139 8.48945 10.2139 8.49403V10.0966L14.2237 7.78046C14.2649 7.75639 14.3096 7.7392 14.3543 7.72544C14.4008 7.7134 14.4472 7.70825 14.4936 7.70825C14.5418 7.70825 14.5882 7.7134 14.6346 7.72544C14.6793 7.7392 14.7223 7.75639 14.7636 7.78046L18.0494 9.67874C18.0787 9.69593 18.1217 9.72 18.1457 9.73548ZM9.45735 7.96101C9.45735 7.91458 9.46423 7.86816 9.47627 7.82173C9.4883 7.77702 9.5055 7.73232 9.52957 7.69105C9.55364 7.6515 9.58115 7.61368 9.61554 7.57929C9.64821 7.54662 9.68604 7.51739 9.72731 7.49503L13.0132 5.59848C13.0441 5.57957 13.0871 5.55549 13.1112 5.54346C12.6607 5.1669 12.1105 4.92618 11.5276 4.85224C10.9447 4.77658 10.3532 4.86943 9.82188 5.11875C9.28885 5.36807 8.83835 5.76527 8.52369 6.26047C8.20903 6.75739 8.04224 7.33169 8.04224 7.91974V11.7541C8.04339 11.7587 8.04454 11.7627 8.04568 11.7661C8.04683 11.7696 8.04855 11.773 8.05084 11.7765C8.05313 11.7799 8.056 11.7833 8.05944 11.7868C8.06173 11.7891 8.06517 11.7914 8.06976 11.7937L9.45735 12.5949V7.96101ZM10.2105 13.0282L11.997 14.0599L13.7835 13.0282V10.9666L11.9987 9.93493L10.2122 10.9666L10.2105 13.0282Z" fill="white"/> +</svg> diff --git a/web/app/components/base/icons/assets/public/tracing/aliyun-icon-big.svg b/web/app/components/base/icons/assets/public/tracing/aliyun-icon-big.svg new file mode 100644 index 0000000000..210a1cd00b --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/aliyun-icon-big.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="159" height="24" viewBox="0 0 159 24"><defs><clipPath id="master_svg0_42_18775"><rect x="0" y="0" width="28.5" height="24" rx="0"/></clipPath></defs><g><g clip-path="url(#master_svg0_42_18775)"><g><path d="M6.10294,22C5.68819,22,5.18195,21.8532,4.58421,21.5595C4.46861,21.5027,4.39038,21.4658,4.34951,21.4488C3.49789,21.0943,2.74367,20.5941,2.08684,19.9484C0.695613,18.5806,0,16.9311,0,15C0,13.0689,0.695612,11.4194,2.08684,10.0516C3.24259,8.91537,4.59607,8.2511,6.14728,8.05878C6.34758,5.97414,7.22633,4.16634,8.78354,2.63539C10.5706,0.878463,12.7286,0,15.2573,0C17.2884,0,19.1146,0.595472,20.7358,1.78642C22.327,2.95528,23.4151,4.46783,24,6.32406L22.0568,6.91594C21.6024,5.47377,20.7561,4.29798,19.5181,3.38858C18.2579,2.46286,16.8377,2,15.2573,2C13.2903,2,11.6119,2.6832,10.222,4.04961C8.83217,5.41601,8.13725,7.06614,8.13725,9L8.13725,10L7.12009,10C5.71758,10,4.51932,10.4886,3.52532,11.4659C2.53132,12.4431,2.03431,13.6211,2.03431,15C2.03431,16.3789,2.53132,17.5569,3.52532,18.5341C3.99531,18.9962,4.53447,19.3538,5.14278,19.6071C5.2229,19.6405,5.33983,19.695,5.49356,19.7705C5.80505,19.9235,6.00818,20,6.10294,20L6.10294,22Z" fill-rule="evenodd" fill="#000000" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M20.18796103515625,11.66909C19.46346103515625,11.5762,18.72726103515625,11.52975,17.991011035156248,11.52975C16.728921035156247,11.52975,15.45515103515625,11.66909,14.23981103515625,11.91292C13.02447103515625,12.156749999999999,11.85588103515625,12.539909999999999,10.73402103515625,12.98113C9.98612103515625,13.306239999999999,9.23822103515625,13.69327,8.49031803515625,14.14223C7.99950790415625,14.43251,7.85927603515625,15.10595,8.15142503515625,15.59361L11.11966103515625,19.9478C11.45855103515625,20.4354,12.13634103515625,20.5747,12.627151035156249,20.2845C12.821921035156251,20.152900000000002,13.14523103515625,19.990299999999998,13.59708103515625,19.796799999999998C14.27487103515625,19.506500000000003,14.964341035156249,19.3091,15.68887103515625,19.169800000000002C16.413401035156248,19.018900000000002,17.14962103515625,18.926000000000002,17.93258103515625,18.926000000000002L20.071061035156248,11.715530000000001L20.18796103515625,11.66909ZM22.91076103515625,12.20319L22.525161035156252,8L20.18796103515625,11.6807C20.72556103515625,11.72714,21.21636103515625,11.82003,21.74216103515625,11.92453C21.74216103515625,11.91679,22.13166103515625,12.00968,22.91076103515625,12.20319ZM18.09616103515625,18.9724L17.06782103515625,22.4557L18.773961035156248,24L21.11116103515625,23.465899999999998L21.788961035156248,19.5414C21.298161035156248,19.402,20.81896103515625,19.2511,20.32816103515625,19.1582C19.60366103515625,19.076900000000002,18.86746103515625,18.9724,18.09616103515625,18.9724ZM27.49166103515625,14.14223C26.74376103515625,13.69327,25.99586103515625,13.306239999999999,25.24796103515625,12.98113C24.52346103515625,12.69086,23.74046103515625,12.40058,22.95756103515625,12.20319L22.95756103515625,12.40058L21.69546103515625,19.5646C21.89416103515625,19.6575,22.139561035156248,19.7039,22.32646103515625,19.8084C22.77836103515625,20.0019,23.101661035156248,20.1645,23.29646103515625,20.2961C23.78726103515625,20.586399999999998,24.51176103515625,20.4354,24.80396103515625,19.959400000000002L27.77216103515625,15.605229999999999C28.16946103515625,15.05951,28.02926103515625,14.43251,27.49166103515625,14.14223Z" fill="#000000" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g><g><path d="M53.295,19.1189814453125L51.951,21.2189814453125Q46.05,17.6279814453125,43.971000000000004,13.0079814453125Q42.921,15.4019814453125,40.884,17.3969814453125Q38.847,19.3919814453125,35.97,21.2399814453125L34.5,19.1609814453125Q41.997,14.9609814453125,42.585,9.2489814453125L35.214,9.2489814453125L35.214,7.1069814453125L42.647999999999996,7.1069814453125L42.647999999999996,2.2979812453125L44.958,2.3819804453125L44.958,7.1069814453125L52.455,7.1069814453125L52.455,9.2489814453125L44.916,9.2489814453125L44.894999999999996,9.5219814453125Q45.650999999999996,12.6509814453125,47.646,14.8979814453125Q49.641,17.1449814453125,53.295,19.1189814453125ZM66.021,7.0649814453125L64.215,7.0649814453125L64.215,5.9099814453125L61.653,5.9099814453125L61.653,4.1039814453125L64.215,4.1039814453125L64.215,2.2559814453125L66.021,2.3399810453125L66.021,4.1039814453125L68.77199999999999,4.1039814453125L68.77199999999999,2.2559814453125L70.557,2.3399810453125L70.557,4.1039814453125L73.413,4.1039814453125L73.413,5.9099814453125L70.557,5.9099814453125L70.557,7.0649814453125L68.77199999999999,7.0649814453125L68.77199999999999,5.9099814453125L66.021,5.9099814453125L66.021,7.0649814453125ZM68.814,16.8929814453125Q69.549,17.9009814453125,70.84049999999999,18.6044814453125Q72.132,19.3079814453125,74.19,19.7279814453125L73.62299999999999,21.6179814453125Q69.36,20.5679814453125,67.449,18.1109814453125Q66.693,19.2449814453125,65.202,20.1059814453125Q63.711,20.9669814453125,61.296,21.6389814453125L60.54,19.8119814453125Q62.766,19.3289814453125,64.0575,18.6044814453125Q65.349,17.879981445312502,65.895,16.8929814453125L61.317,16.8929814453125L61.317,15.2339814453125L66.378,15.2339814453125Q66.399,15.1499814453125,66.399,15.0029814453125Q66.42,14.7299814453125,66.42,13.9949814453125L62.262,13.9949814453125L62.262,12.4199814453125L60.96,13.3439814453125Q60.519,12.3779814453125,59.784,11.2439814453125L59.784,21.2189814453125L57.957,21.2189814453125L57.957,12.0839814453125Q56.949,14.6669814453125,55.962,16.3049814453125L54.45,14.7929814453125Q55.332,13.3649814453125,56.193,11.5904814453125Q57.054,9.815981445312499,57.620999999999995,8.1779814453125L55.521,8.1779814453125L55.521,6.2669814453125L57.957,6.2669814453125L57.957,2.3189811453125L59.784,2.4029824453125L59.784,6.2669814453125L61.757999999999996,6.2669814453125L61.757999999999996,8.1779814453125L59.784,8.1779814453125L59.784,10.3829814453125L60.708,9.6689814453125Q61.59,10.7609814453125,62.262,12.0419814453125L62.262,7.5479814453125L72.489,7.5479814453125L72.489,13.9949814453125L68.37299999999999,13.9949814453125Q68.331,14.7089814453125,68.331,15.0029814453125L68.331,15.2339814453125L73.497,15.2339814453125L73.497,16.8929814453125L68.814,16.8929814453125ZM70.809,10.1099814453125L70.809,9.1019814453125L64.005,9.1019814453125L64.005,10.1099814453125L70.809,10.1099814453125ZM70.809,11.4749814453125L64.005,11.4749814453125L64.005,12.4409814453125L70.809,12.4409814453125L70.809,11.4749814453125ZM88.89,13.7639814453125L88.30199999999999,11.8529814453125L89.856,11.7269814453125Q90.63300000000001,11.6639814453125,90.88499999999999,11.4644814453125Q91.137,11.2649814453125,91.137,10.5929814453125L91.137,2.6969814453125L93.09,2.7809824453125L93.09,11.1179814453125Q93.09,12.0839814453125,92.85900000000001,12.5879814453125Q92.628,13.0919814453125,92.0715,13.3229814453125Q91.515,13.5539814453125,90.444,13.6379814453125L88.89,13.7639814453125ZM76.35300000000001,13.5959814453125Q77.445,12.4619814453125,77.928,11.6639814453125Q78.411,10.8659814453125,78.55799999999999,9.8579814453125L76.311,9.8579814453125L76.311,8.0309814453125L78.684,8.0309814453125L78.684,7.4639814453125L78.684,5.2589814453125L76.836,5.2589814453125L76.836,3.3689814453125L86.706,3.3689814453125L86.706,5.2589814453125L84.9,5.2589814453125L84.9,8.0309814453125L87.126,8.0309814453125L87.126,9.8579814453125L84.9,9.8579814453125L84.9,13.4909814453125L82.926,13.4909814453125L82.926,9.8579814453125L80.532,9.8579814453125Q80.364,11.3699814453125,79.797,12.4619814453125Q79.22999999999999,13.5539814453125,77.949,14.8349814453125L76.35300000000001,13.5959814453125ZM87.672,3.7679814453125L89.583,3.8519814453125L89.583,11.0969814453125L87.672,11.0969814453125L87.672,3.7679814453125ZM80.637,5.2589814453125L80.637,7.4849814453125L80.637,8.0309814453125L82.926,8.0309814453125L82.926,5.2589814453125L80.637,5.2589814453125ZM86.223,16.7039814453125L86.223,18.9719814453125L94.32900000000001,18.9719814453125L94.32900000000001,20.8409814453125L76.017,20.8409814453125L76.017,18.9719814453125L84.144,18.9719814453125L84.144,16.7039814453125L78.15899999999999,16.7039814453125L78.15899999999999,14.8769814453125L84.144,14.8769814453125L84.144,13.6799814453125L86.223,13.7639814453125L86.223,14.8769814453125L92.229,14.8769814453125L92.229,16.7039814453125L86.223,16.7039814453125ZM115.119,3.4739814453125L115.119,5.5319814453125L112.494,5.5319814453125L112.494,18.0899814453125Q112.494,19.3289814453125,112.2315,19.9169814453125Q111.969,20.5049814453125,111.3075,20.7569814453125Q110.646,21.0089814453125,109.239,21.1349814453125L107.874,21.2609814453125L107.223,19.1819814453125L108.819,19.0559814453125Q109.554,18.9929814453125,109.8795,18.8669814453125Q110.205,18.7409814453125,110.31,18.4469814453125Q110.415,18.1529814453125,110.415,17.501981445312502L110.415,5.5319814453125L96.59700000000001,5.5319814453125L96.59700000000001,3.4739814453125L115.119,3.4739814453125ZM98.802,7.9679814453125L107.433,7.9679814453125L107.433,17.2499814453125L98.802,17.2499814453125L98.802,7.9679814453125ZM100.797,15.2129814453125L105.459,15.2129814453125L105.459,10.0259814453125L100.797,10.0259814453125L100.797,15.2129814453125ZM132.192,5.1539814453125L126.711,5.1539814453125L126.711,15.1289814453125L124.737,15.1289814453125L124.737,3.1799814453125L134.166,3.1799814453125L134.166,15.0869814453125L132.192,15.0869814453125L132.192,5.1539814453125ZM123.036,18.6569814453125Q122.385,17.2499814453125,121.482,15.4649814453125Q120.327,17.9009814453125,118.311,20.8199814453125L116.715,19.4549814453125Q119.088,16.2839814453125,120.369,13.2179814453125Q118.584,9.7739814453125,117.534,8.0099814453125L119.067,7.0229814453125Q119.76,8.1149814453125,121.251,10.7609814453125Q121.839,8.7449814453125,122.217,6.0989814453125L117.576,6.0989814453125L117.576,4.0829814453125L124.254,4.0829814453125L124.254,6.0989814453125Q123.75,9.8579814453125,122.511,13.0919814453125Q123.771,15.4439814453125,124.695,17.3549814453125L123.036,18.6569814453125ZM135.78300000000002,16.5779814453125Q135.72,17.8379814453125,135.594,18.6359814453125Q135.46800000000002,19.6019814453125,135.237,20.0849814453125Q135.006,20.5679814453125,134.523,20.7779814453125Q134.04000000000002,20.9879814453125,133.095,20.9879814453125L131.247,20.9879814453125Q130.05,20.9879814453125,129.5775,20.4839814453125Q129.10500000000002,19.9799814453125,129.10500000000002,18.6359814453125L129.10500000000002,16.3469814453125Q128.349,17.8379814453125,127.068,19.1399814453125Q125.787,20.4419814453125,123.834,21.7439814453125L122.532,20.0219814453125Q124.863,18.5939814453125,126.0705,17.2394814453125Q127.278,15.8849814453125,127.74,14.2994814453125Q128.202,12.7139814453125,128.286,10.2569814453125L128.349,6.1409814453125L130.449,6.224981445312499L130.386,10.5089814453125Q130.32299999999998,12.2309814453125,130.05,13.5959814453125L131.058,13.6379814453125L131.058,17.9219814453125Q131.058,18.5939814453125,131.226,18.7829814453125Q131.394,18.9719814453125,131.982,18.9719814453125L132.696,18.9719814453125Q133.263,18.9719814453125,133.4625,18.7934814453125Q133.662,18.6149814453125,133.74599999999998,17.942981445312498Q133.872,16.7249814453125,133.872,15.8639814453125L135.78300000000002,16.5779814453125ZM139.374,2.5079814453125Q140.088,2.9909814453125,141.054,3.8204814453125Q142.01999999999998,4.6499814453125,142.587,5.2379814453125L141.39,6.8759814453125Q140.928,6.3089814453125,139.941,5.3954814453125Q138.954,4.4819814453125,138.28199999999998,3.9569814453125L139.374,2.5079814453125ZM152.184,19.0769814453125Q152.751,19.0139814453125,153.014,18.9299814453125Q153.276,18.8459814453125,153.381,18.6359814453125Q153.486,18.4259814453125,153.486,17.9639814453125L153.486,2.6549814453125L155.124,2.7389824453125L155.124,18.5939814453125Q155.124,19.5389814453125,154.95600000000002,20.0009814453125Q154.788,20.4629814453125,154.315,20.6729814453125Q153.84300000000002,20.8829814453125,152.83499999999998,20.9669814453125L151.659,21.0509814453125L151.09199999999998,19.1609814453125L152.184,19.0769814453125ZM142.587,15.8429814453125L142.587,3.4529814453125L149.286,3.4529814453125L149.286,15.7799814453125L147.543,15.7799814453125L147.543,5.2799814453125L144.288,5.2799814453125L144.288,15.8429814453125L142.587,15.8429814453125ZM150.546,16.4099814453125L150.546,4.4819814453125L152.184,4.5659814453125005L152.184,16.4099814453125L150.546,16.4099814453125ZM141.012,19.7279814453125Q142.81799999999998,18.4049814453125,143.679,17.3654814453125Q144.54000000000002,16.3259814453125,144.834,15.0974814453125Q145.128,13.8689814453125,145.128,11.7689814453125L145.128,6.224981445312499L146.76600000000002,6.3089814453125L146.76600000000002,11.7689814453125Q146.76600000000002,14.2889814453125,146.33499999999998,15.8954814453125Q145.905,17.501981445312502,144.95,18.6779814453125Q143.994,19.8539814453125,142.209,21.1979814453125L141.012,19.7279814453125ZM138.639,7.2329814453125Q139.353,7.7369814453125,140.329,8.5874814453125Q141.30599999999998,9.4379814453125,141.957,10.1099814453125L140.76,11.7899814453125Q140.151,11.0969814453125,139.174,10.2044814453125Q138.19799999999998,9.311981445312501,137.421,8.7239814453125L138.639,7.2329814453125ZM137.82,20.2949814453125Q138.156,19.3709814453125,138.933,16.5989814453125Q139.70999999999998,13.8269814453125,139.878,12.9029814453125L140.781,13.1969814453125L141.642,13.4909814453125Q141.369,14.7299814453125,140.66500000000002,17.2814814453125Q139.962,19.8329814453125,139.60500000000002,20.9249814453125L137.82,20.2949814453125ZM147.144,15.9689814453125Q148.86599999999999,17.5439814453125,150.10500000000002,19.1189814453125L148.86599999999999,20.4839814453125Q148.06799999999998,19.4129814453125,147.449,18.6884814453125Q146.829,17.9639814453125,146.01,17.207981445312498L147.144,15.9689814453125Z" fill="#000000" fill-opacity="1"/></g></g></svg> diff --git a/web/app/components/base/icons/assets/public/tracing/aliyun-icon.svg b/web/app/components/base/icons/assets/public/tracing/aliyun-icon.svg new file mode 100644 index 0000000000..6f7645301c --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/aliyun-icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="106" height="16" viewBox="0 0 106 16"><defs><clipPath id="master_svg0_36_00924"><rect x="0" y="0" width="19" height="16" rx="0"/></clipPath></defs><g><g clip-path="url(#master_svg0_36_00924)"><g><g><path d="M4.06862,14.6667C3.79213,14.6667,3.45463,14.5688,3.05614,14.373C2.97908,14.3351,2.92692,14.3105,2.89968,14.2992C2.33193,14.0628,1.82911,13.7294,1.39123,13.2989C0.463742,12.3871,0,11.2874,0,10C0,8.71258,0.463742,7.61293,1.39123,6.70107C2.16172,5.94358,3.06404,5.50073,4.09819,5.37252C4.23172,3.98276,4.81755,2.77756,5.85569,1.75693C7.04708,0.585642,8.4857,0,10.1716,0C11.5256,0,12.743,0.396982,13.8239,1.19095C14.8847,1.97019,15.61,2.97855,16,4.21604L14.7045,4.61063C14.4016,3.64918,13.8374,2.86532,13.0121,2.25905C12.1719,1.64191,11.2251,1.33333,10.1716,1.33333C8.8602,1.33333,7.74124,1.7888,6.81467,2.69974C5.88811,3.61067,5.42483,4.71076,5.42483,6L5.42483,6.66667L4.74673,6.66667C3.81172,6.66667,3.01288,6.99242,2.35021,7.64393C1.68754,8.2954,1.35621,9.08076,1.35621,10C1.35621,10.9192,1.68754,11.7046,2.35021,12.3561C2.66354,12.6641,3.02298,12.9026,3.42852,13.0714C3.48193,13.0937,3.55988,13.13,3.66237,13.1803C3.87004,13.2823,4.00545,13.3333,4.06862,13.3333L4.06862,14.6667Z" fill-rule="evenodd" fill="#000000" fill-opacity="1" style="mix-blend-mode:passthrough"/></g><g><path d="M13.458613505859375,7.779393492279053C12.975613505859375,7.717463492279053,12.484813505859375,7.686503492279053,11.993983505859376,7.686503492279053C11.152583505859376,7.686503492279053,10.303403505859375,7.779393492279053,9.493183505859374,7.941943492279053C8.682953505859375,8.104503492279052,7.903893505859375,8.359943492279053,7.155983505859375,8.654083492279053C6.657383505859375,8.870823492279053,6.158783505859375,9.128843492279053,5.660181505859375,9.428153492279053C5.332974751859375,9.621673492279053,5.239486705859375,10.070633492279054,5.434253505859375,10.395743492279053L7.413073505859375,13.298533492279052C7.639003505859375,13.623603492279052,8.090863505859375,13.716463492279052,8.418073505859375,13.523003492279052C8.547913505859375,13.435263492279052,8.763453505859374,13.326893492279053,9.064693505859374,13.197863492279053C9.516553505859374,13.004333492279052,9.976203505859374,12.872733492279053,10.459223505859375,12.779863492279052C10.942243505859375,12.679263492279052,11.433053505859375,12.617333492279052,11.955023505859375,12.617333492279052L13.380683505859375,7.810353492279052L13.458613505859375,7.779393492279053ZM15.273813505859374,8.135463492279053L15.016753505859375,5.333333492279053L13.458613505859375,7.787133492279053C13.817013505859375,7.818093492279052,14.144213505859375,7.880023492279053,14.494743505859375,7.949683492279053C14.494743505859375,7.944523492279053,14.754433505859375,8.006453492279054,15.273813505859374,8.135463492279053ZM12.064083505859376,12.648273492279053L11.378523505859375,14.970463492279054L12.515943505859376,16.00003349227905L14.074083505859376,15.643933492279054L14.525943505859376,13.027603492279052C14.198743505859374,12.934663492279054,13.879283505859375,12.834063492279054,13.552083505859375,12.772133492279053C13.069083505859375,12.717933492279052,12.578283505859375,12.648273492279053,12.064083505859376,12.648273492279053ZM18.327743505859374,9.428153492279053C17.829143505859374,9.128843492279053,17.330543505859374,8.870823492279053,16.831943505859375,8.654083492279053C16.348943505859374,8.460573492279053,15.826943505859376,8.267053492279054,15.305013505859375,8.135463492279053L15.305013505859375,8.267053492279054L14.463613505859374,13.043063492279053C14.596083505859376,13.105003492279053,14.759683505859375,13.135933492279053,14.884283505859376,13.205603492279053C15.185523505859376,13.334623492279052,15.401043505859375,13.443003492279052,15.530943505859375,13.530733492279053C15.858143505859376,13.724263492279054,16.341143505859375,13.623603492279052,16.535943505859375,13.306263492279053L18.514743505859375,10.403483492279053C18.779643505859376,10.039673492279054,18.686143505859377,9.621673492279053,18.327743505859374,9.428153492279053Z" fill="#000000" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></g><g><g><path d="M36.174,12.958L35.278,14.358Q31.344,11.964,29.958,8.884Q29.258,10.48,27.9,11.81Q26.542,13.14,24.624,14.372L23.644,12.986Q28.642,10.186,29.034,6.378L24.12,6.378L24.12,4.95L29.076,4.95L29.076,1.743999L30.616,1.7999990000000001L30.616,4.95L35.614000000000004,4.95L35.614000000000004,6.378L30.588,6.378L30.573999999999998,6.56Q31.078,8.646,32.408,10.144Q33.738,11.642,36.174,12.958ZM44.658,4.922000000000001L43.454,4.922000000000001L43.454,4.152L41.745999999999995,4.152L41.745999999999995,2.948L43.454,2.948L43.454,1.716L44.658,1.771999L44.658,2.948L46.492000000000004,2.948L46.492000000000004,1.716L47.682,1.771999L47.682,2.948L49.586,2.948L49.586,4.152L47.682,4.152L47.682,4.922000000000001L46.492000000000004,4.922000000000001L46.492000000000004,4.152L44.658,4.152L44.658,4.922000000000001ZM46.519999999999996,11.474Q47.010000000000005,12.146,47.870999999999995,12.615Q48.732,13.084,50.104,13.364L49.726,14.624Q46.884,13.924,45.61,12.286Q45.106,13.042,44.111999999999995,13.616Q43.117999999999995,14.19,41.507999999999996,14.638L41.004000000000005,13.42Q42.488,13.098,43.349000000000004,12.615Q44.21,12.132,44.574,11.474L41.522,11.474L41.522,10.368L44.896,10.368Q44.91,10.312,44.91,10.214Q44.924,10.032,44.924,9.542L42.152,9.542L42.152,8.492L41.284,9.108Q40.989999999999995,8.464,40.5,7.708L40.5,14.358L39.282,14.358L39.282,8.268Q38.61,9.99,37.952,11.082L36.944,10.074Q37.532,9.122,38.106,7.939Q38.68,6.756,39.058,5.664L37.658,5.664L37.658,4.390000000000001L39.282,4.390000000000001L39.282,1.7579989999999999L40.5,1.814L40.5,4.390000000000001L41.816,4.390000000000001L41.816,5.664L40.5,5.664L40.5,7.134L41.116,6.658Q41.704,7.386,42.152,8.24L42.152,5.244L48.97,5.244L48.97,9.542L46.226,9.542Q46.198,10.018,46.198,10.214L46.198,10.368L49.641999999999996,10.368L49.641999999999996,11.474L46.519999999999996,11.474ZM47.85,6.952L47.85,6.28L43.314,6.28L43.314,6.952L47.85,6.952ZM47.85,7.862L43.314,7.862L43.314,8.506L47.85,8.506L47.85,7.862ZM59.904,9.388L59.512,8.114L60.548,8.030000000000001Q61.066,7.988,61.234,7.855Q61.402,7.722,61.402,7.274L61.402,2.01L62.704,2.066L62.704,7.624Q62.704,8.268,62.55,8.604Q62.396,8.940000000000001,62.025,9.094Q61.654,9.248,60.94,9.304L59.904,9.388ZM51.546,9.276Q52.274,8.52,52.596000000000004,7.988Q52.918,7.456,53.016,6.784L51.518,6.784L51.518,5.566L53.1,5.566L53.1,5.188L53.1,3.718L51.867999999999995,3.718L51.867999999999995,2.458L58.448,2.458L58.448,3.718L57.244,3.718L57.244,5.566L58.728,5.566L58.728,6.784L57.244,6.784L57.244,9.206L55.928,9.206L55.928,6.784L54.332,6.784Q54.22,7.792,53.842,8.52Q53.464,9.248,52.61,10.102L51.546,9.276ZM59.092,2.724L60.366,2.7800000000000002L60.366,7.61L59.092,7.61L59.092,2.724ZM54.402,3.718L54.402,5.202L54.402,5.566L55.928,5.566L55.928,3.718L54.402,3.718ZM58.126,11.348L58.126,12.86L63.53,12.86L63.53,14.106L51.322,14.106L51.322,12.86L56.74,12.86L56.74,11.348L52.75,11.348L52.75,10.13L56.74,10.13L56.74,9.332L58.126,9.388L58.126,10.13L62.13,10.13L62.13,11.348L58.126,11.348ZM77.39,2.528L77.39,3.9L75.64,3.9L75.64,12.272Q75.64,13.098,75.465,13.49Q75.28999999999999,13.882,74.84899999999999,14.05Q74.408,14.218,73.47,14.302L72.56,14.386L72.126,13L73.19,12.916Q73.68,12.874,73.89699999999999,12.79Q74.114,12.706,74.184,12.51Q74.25399999999999,12.314,74.25399999999999,11.88L74.25399999999999,3.9L65.042,3.9L65.042,2.528L77.39,2.528ZM66.512,5.524L72.26599999999999,5.524L72.26599999999999,11.712L66.512,11.712L66.512,5.524ZM67.842,10.354L70.95,10.354L70.95,6.896L67.842,6.896L67.842,10.354ZM88.772,3.648L85.118,3.648L85.118,10.298L83.80199999999999,10.298L83.80199999999999,2.332L90.088,2.332L90.088,10.27L88.772,10.27L88.772,3.648ZM82.668,12.65Q82.23400000000001,11.712,81.632,10.522Q80.862,12.146,79.518,14.092L78.45400000000001,13.182Q80.036,11.068,80.89,9.024Q79.7,6.728,79,5.552L80.02199999999999,4.894Q80.48400000000001,5.622,81.47800000000001,7.386Q81.87,6.042,82.122,4.2780000000000005L79.02799999999999,4.2780000000000005L79.02799999999999,2.934L83.47999999999999,2.934L83.47999999999999,4.2780000000000005Q83.144,6.784,82.318,8.940000000000001Q83.158,10.508,83.774,11.782L82.668,12.65ZM91.166,11.264Q91.124,12.104,91.04,12.636Q90.956,13.28,90.802,13.602Q90.648,13.924,90.326,14.064Q90.004,14.204,89.374,14.204L88.142,14.204Q87.344,14.204,87.029,13.868Q86.714,13.532,86.714,12.636L86.714,11.11Q86.21000000000001,12.104,85.356,12.972Q84.50200000000001,13.84,83.2,14.708L82.332,13.56Q83.886,12.608,84.691,11.705Q85.49600000000001,10.802,85.804,9.745Q86.112,8.687999999999999,86.168,7.05L86.21000000000001,4.306L87.61,4.362L87.568,7.218Q87.526,8.366,87.344,9.276L88.016,9.304L88.016,12.16Q88.016,12.608,88.128,12.734Q88.24,12.86,88.632,12.86L89.108,12.86Q89.486,12.86,89.619,12.741Q89.752,12.622,89.808,12.174Q89.892,11.362,89.892,10.788L91.166,11.264ZM93.56,1.884Q94.036,2.206,94.68,2.759Q95.324,3.312,95.702,3.704L94.904,4.795999999999999Q94.596,4.418,93.938,3.809Q93.28,3.2,92.832,2.85L93.56,1.884ZM102.1,12.93Q102.478,12.888,102.653,12.832Q102.828,12.776,102.898,12.636Q102.968,12.496,102.968,12.188L102.968,1.981999L104.06,2.0380000000000003L104.06,12.608Q104.06,13.238,103.948,13.546Q103.836,13.854,103.521,13.994Q103.206,14.134,102.534,14.19L101.75,14.246L101.372,12.986L102.1,12.93ZM95.702,10.774L95.702,2.5140000000000002L100.168,2.5140000000000002L100.168,10.732L99.006,10.732L99.006,3.732L96.836,3.732L96.836,10.774L95.702,10.774ZM101.008,11.152L101.008,3.2L102.1,3.256L102.1,11.152L101.008,11.152ZM94.652,13.364Q95.856,12.482,96.43,11.789Q97.004,11.096,97.2,10.277Q97.396,9.458,97.396,8.058L97.396,4.362L98.488,4.418L98.488,8.058Q98.488,9.738,98.201,10.809Q97.914,11.88,97.277,12.664Q96.64,13.448,95.45,14.344L94.652,13.364ZM93.07,5.034Q93.546,5.37,94.197,5.937Q94.848,6.504,95.282,6.952L94.484,8.072Q94.078,7.61,93.427,7.015Q92.776,6.42,92.258,6.028L93.07,5.034ZM92.524,13.742Q92.748,13.126,93.266,11.278Q93.784,9.43,93.896,8.814L94.498,9.01L95.072,9.206Q94.89,10.032,94.421,11.733Q93.952,13.434,93.714,14.162L92.524,13.742ZM98.74,10.858Q99.888,11.908,100.714,12.958L99.888,13.868Q99.356,13.154,98.943,12.671Q98.53,12.188,97.984,11.684L98.74,10.858Z" fill="#000000" fill-opacity="1"/></g></g></g></svg> diff --git a/web/app/components/base/icons/assets/public/tracing/arize-icon-big.svg b/web/app/components/base/icons/assets/public/tracing/arize-icon-big.svg new file mode 100644 index 0000000000..80667847ab --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/arize-icon-big.svg @@ -0,0 +1,17 @@ +<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111 24" width="111" height="24"> + <g id="Layer_1-2" data-name="Layer 1"> + <rect style="fill: none;" y="0" width="111" height="24"/> + <g id="Arize_-_standard" data-name="Arize - standard"> + <g> + <path d="M100.94,14.55h-11.29c0,.13-.01.23.02.31.53,1.55,1.54,2.59,3.19,2.88,1.7.29,3.22,0,4.3-1.52.09-.13.28-.27.43-.28.99-.02,1.99-.01,2.99-.01-.16,2.69-3.89,4.98-7.6,4.7-3.99-.3-6.91-3.79-6.58-7.88.37-4.62,3.67-7.31,8.37-6.85,4.05.4,6.68,4.04,6.19,8.64ZM89.71,11.66h7.96c-.43-1.88-2.04-3.02-4.17-2.97-1.87.04-3.48,1.3-3.78,2.97Z"/> + <path style="fill: #ff008c;" d="M20.81,2.53c.59-.03,1.11.26,1.49.8,3.05,4.38,6.09,8.77,9.13,13.16.63.91.43,1.99-.43,2.59-.84.59-1.97.35-2.62-.57-2.1-3.01-4.19-6.04-6.28-9.05-.04-.06-.08-.11-.12-.17-.83-1.17-1.65-1.18-2.47,0-2.11,3.05-4.23,6.09-6.34,9.14-.35.5-.77.88-1.4.95-.77.08-1.39-.19-1.79-.86-.41-.69-.38-1.38.07-2.03,1.01-1.47,2.04-2.93,3.06-4.4,2.01-2.89,4.03-5.77,6.03-8.67.39-.56.88-.9,1.67-.89Z"/> + <path d="M52.84,20.43h-3.02v-1.16c-.1.02-.16.01-.19.03-2.46,1.73-5.09,1.88-7.68.51-2.61-1.38-3.71-3.77-3.68-6.67.03-3.26,1.82-5.96,4.64-6.86,2.35-.75,4.64-.67,6.69.93.04.03.09.03.2.07v-1.18h3.03v14.31ZM41.36,13.32c.01.17.02.46.05.75.22,2.09,1.76,3.58,3.91,3.76,2.1.18,3.8-.95,4.38-2.96.14-.47.2-.98.22-1.47.13-3.22-2.25-5.27-5.33-4.61-1.92.41-3.19,2.14-3.23,4.53Z"/> + <path d="M80.41,8.9h-7.44v-2.77h11.64c0,.81.02,1.61-.01,2.41,0,.17-.19.35-.32.51-2.28,2.68-4.57,5.36-6.85,8.04-.11.13-.21.26-.38.47h7.53v2.86h-11.74c0-.82-.02-1.62.01-2.42,0-.16.17-.33.28-.47,2.32-2.74,4.64-5.47,6.96-8.21.09-.1.16-.21.32-.42Z"/> + <path d="M59.39,20.54h-3.03V6.22h3.03v1.54c.8-.48,1.55-1.01,2.37-1.41.85-.41,1.79-.41,2.77-.35v2.94c-.16.01-.3.03-.45.03-2.37.03-4.01,1.36-4.51,3.69-.12.57-.16,1.16-.16,1.74-.02,1.85,0,3.71,0,5.56v.57Z"/> + <path d="M67.1,6.09h2.99v14.33h-2.99V6.09Z"/> + <path style="fill: #ff008c;" d="M20.73,19.53c1.25,0,2.24.96,2.25,2.19.01,1.24-1.02,2.29-2.24,2.28-1.23-.01-2.21-1.01-2.22-2.24,0-1.25.96-2.22,2.21-2.22Z"/> + <path d="M70.7,2.11c0,1.19-.92,2.14-2.09,2.15-1.19.01-2.16-.95-2.16-2.14C66.46.95,67.4,0,68.58,0c1.18,0,2.12.93,2.12,2.11Z"/> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/web/app/components/base/icons/assets/public/tracing/arize-icon.svg b/web/app/components/base/icons/assets/public/tracing/arize-icon.svg new file mode 100644 index 0000000000..f43e28ff83 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/arize-icon.svg @@ -0,0 +1,17 @@ +<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 74 16" width="74" height="16"> + <g id="Layer_1-2" data-name="Layer 1"> + <rect style="fill: none;" y="0" width="74" height="16"/> + <g id="Arize_-_standard" data-name="Arize - standard"> + <g> + <path d="M67.29,9.7h-7.52c0,.08,0,.15.01.21.35,1.03,1.03,1.73,2.13,1.92,1.13.19,2.14,0,2.86-1.01.06-.09.19-.18.29-.19.66-.02,1.33,0,1.99,0-.1,1.79-2.59,3.32-5.07,3.14-2.66-.2-4.61-2.53-4.39-5.25.25-3.08,2.44-4.88,5.58-4.56,2.7.27,4.45,2.69,4.12,5.76ZM59.81,7.77h5.3c-.28-1.25-1.36-2.01-2.78-1.98-1.25.03-2.32.87-2.52,1.98Z"/> + <path style="fill: #ff008c;" d="M13.87,1.69c.4-.02.74.17.99.54,2.03,2.92,4.06,5.85,6.08,8.77.42.61.28,1.33-.29,1.73-.56.39-1.31.24-1.74-.38-1.4-2.01-2.79-4.02-4.19-6.04-.03-.04-.05-.08-.08-.11-.55-.78-1.1-.78-1.64,0-1.41,2.03-2.82,4.06-4.23,6.09-.23.34-.52.59-.93.63-.51.06-.92-.13-1.19-.57-.28-.46-.25-.92.05-1.35.68-.98,1.36-1.96,2.04-2.93,1.34-1.93,2.68-3.85,4.02-5.78.26-.37.59-.6,1.11-.59Z"/> + <path d="M35.23,13.62h-2.01v-.77c-.07.01-.1,0-.13.02-1.64,1.16-3.39,1.25-5.12.34-1.74-.92-2.47-2.51-2.45-4.45.02-2.17,1.22-3.97,3.1-4.57,1.57-.5,3.09-.45,4.46.62.02.02.06.02.14.05v-.79h2.02v9.54ZM27.57,8.88c0,.11.01.31.03.5.14,1.39,1.18,2.39,2.61,2.51,1.4.12,2.53-.63,2.92-1.97.09-.32.14-.65.15-.98.09-2.15-1.5-3.51-3.56-3.07-1.28.27-2.13,1.43-2.15,3.02Z"/> + <path d="M53.61,5.93h-4.96v-1.85h7.76c0,.54.01,1.07,0,1.61,0,.12-.12.24-.21.34-1.52,1.79-3.05,3.57-4.57,5.36-.07.09-.14.18-.26.32h5.02v1.91h-7.83c0-.54-.01-1.08,0-1.61,0-.11.11-.22.19-.31,1.55-1.83,3.1-3.65,4.64-5.47.06-.07.11-.14.21-.28Z"/> + <path d="M39.6,13.69h-2.02V4.15h2.02v1.03c.54-.32,1.04-.68,1.58-.94.57-.28,1.19-.27,1.85-.23v1.96c-.1,0-.2.02-.3.02-1.58.02-2.68.9-3.01,2.46-.08.38-.11.77-.11,1.16-.01,1.24,0,2.47,0,3.71v.38Z"/> + <path d="M44.74,4.06h1.99v9.56h-1.99V4.06Z"/> + <path style="fill: #ff008c;" d="M13.82,13.02c.84,0,1.49.64,1.5,1.46,0,.83-.68,1.53-1.5,1.52-.82,0-1.47-.67-1.48-1.5,0-.83.64-1.48,1.47-1.48Z"/> + <path d="M47.13,1.41c0,.8-.61,1.43-1.39,1.43-.8,0-1.44-.63-1.44-1.43,0-.78.63-1.41,1.42-1.41.79,0,1.41.62,1.41,1.41Z"/> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/web/app/components/base/icons/assets/public/tracing/phoenix-icon-big.svg b/web/app/components/base/icons/assets/public/tracing/phoenix-icon-big.svg new file mode 100644 index 0000000000..d22e928449 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/phoenix-icon-big.svg @@ -0,0 +1,97 @@ +<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 111 24" width="111" height="24"> + <defs> + <linearGradient id="linear-gradient" x1="9.07" y1="1.47" x2="19.77" y2="20" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#11bab5"/> + <stop offset=".5" stop-color="#00adee"/> + <stop offset="1" stop-color="#0094c5"/> + </linearGradient> + <linearGradient id="linear-gradient-2" x1="19.48" y1="16.3" x2="10.46" y2=".67" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#fcfdff" stop-opacity="0"/> + <stop offset=".12" stop-color="#fcfdff" stop-opacity=".17"/> + <stop offset=".3" stop-color="#fcfdff" stop-opacity=".42"/> + <stop offset=".47" stop-color="#fcfdff" stop-opacity=".63"/> + <stop offset=".64" stop-color="#fcfdff" stop-opacity=".79"/> + <stop offset=".78" stop-color="#fcfdff" stop-opacity=".9"/> + <stop offset=".91" stop-color="#fcfdff" stop-opacity=".97"/> + <stop offset="1" stop-color="#fcfdff"/> + </linearGradient> + <linearGradient id="linear-gradient-3" x1="16.82" y1="16.21" x2="10.32" y2="4.94" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-4" x1="15.92" y1="17.03" x2="12.29" y2="10.74" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-5" x1="16.08" y1="18.3" x2="13.82" y2="14.38" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-6" x1="12.26" y1="17.94" x2="12.26" y2="22.07" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".03" stop-color="#231f20" stop-opacity=".9"/> + <stop offset=".22" stop-color="#231f20" stop-opacity=".4"/> + <stop offset=".42" stop-color="#231f20" stop-opacity=".1"/> + <stop offset=".65" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="Fade_to_Black_2" data-name="Fade to Black 2" x1="16.89" y1="17.55" x2="16.89" y2="19.66" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset="1" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="linear-gradient-7" x1="17.91" y1="12.1" x2="17.91" y2="10.38" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".03" stop-color="#231f20" stop-opacity=".95"/> + <stop offset=".51" stop-color="#231f20" stop-opacity=".26"/> + <stop offset=".81" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="linear-gradient-8" x1="21.67" y1="13.37" x2="21.67" y2="9.21" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".18" stop-color="#231f20" stop-opacity=".48"/> + <stop offset=".38" stop-color="#231f20" stop-opacity=".12"/> + <stop offset=".58" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="Fade_to_Black_2-2" data-name="Fade to Black 2" x1="25.54" y1="16.65" x2="24.1" y2="14.16" xlink:href="#Fade_to_Black_2"/> + <linearGradient id="linear-gradient-9" x1="26.7" y1="15.97" x2="24.55" y2="12.24" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-10" x1="21.11" y1="15.34" x2="24.24" y2="13.53" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".31" stop-color="#231f20" stop-opacity=".5"/> + <stop offset=".67" stop-color="#231f20" stop-opacity=".13"/> + <stop offset="1" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + </defs> + <g id="Layer_1-2" data-name="Layer 1"> + <rect style="fill: none;" y=".04" width="111" height="24"/> + <g id="Phoenix_horiz_-_gradient" data-name="Phoenix horiz - gradient"> + <path style="fill: url(#linear-gradient);" d="M26.87,15c-.06-.28-.2-.53-.42-.76-.08-.09-.18-.17-.28-.25-.05-.04-.1-.07-.15-.1-.07-.04-.14-.08-.21-.12-.28-.14-.51-.29-.72-.44-.08-.06-.17-.13-.24-.19-.19-.16-.31-.3-.4-.45-.03-.05-.06-.1-.08-.15-.01-.02-.03-.04-.05-.05-.03-.02-.08-.04-.12-.03-.07.01-.12.07-.13.13,0,.05,0,.11,0,.17,0,.03-.01.07-.04.08,0,0-.01,0-.02,0-.04,0-.08.03-.11.07-.03.04-.03.08-.02.13,0,.04.02.06.03.09v.02s.02.03.03.04c.02.04.04.08.07.12.05.07.1.14.17.22.02.02.02.06,0,.09-.02.03-.05.04-.08.04-.37-.05-.7-.13-1-.25-.04-.01-.07-.03-.11-.04-.28-.12.22-.61.45-.96,1-1.53,1.2-3.29,1.21-5.07,0-1.72-.18-3.41-.52-5.08h0c-.22-1.4-.41-1.93-.53-2.13-.03-.05-.05-.08-.08-.1-.03-.02-.05-.01-.05-.01-.06,0-.14.06-.23.15-.37.39-.35.86-.25,1.34.47,2.21.82,4.43.66,6.7-.11,1.56-.4,3.07-1.5,4.3-.07.07-.14.14-.21.21-.08.08-.21.24-.3.17-.13-.09-.01-.27.03-.36.64-1.5.82-3.07.73-4.69,0-.04,0-.07,0-.11h0s0-.06,0-.09c-.02-.21-.04-.43-.06-.64-.28-2.56-.61-2.73-.61-2.73h0s-.05-.03-.09-.04c-.17-.04-.27.12-.35.23-.35.52-.19,1.07-.08,1.62.39,1.92.4,3.82-.28,5.69-.06.17-.14.34-.21.5-.08.17-.14.39-.42.3-.26-.09-.19-.29-.16-.47.09-.47.16-.93.2-1.4h0s0-.09.01-.13c0-.08.01-.16.02-.24,0-.07,0-.13.01-.2,0-.03,0-.07,0-.1.05-1.48-.2-2.16-.29-2.36-.01-.02-.02-.05-.03-.07h0s0,0,0,0c-.1-.15-.26-.13-.4,0-.26.25-.4.56-.33.93.19,1.1.08,2.2-.13,3.28-.03.15-.09.41-.4.34-.21-.05-.26-.14-.27-.32,0-.14,0-.28,0-.42,0,0,0,0,0-.01h0s0-.04,0-.06h0s0-.02,0-.03c0-.19-.02-.38-.05-.57,0-.02,0-.04,0-.05-.12-1.07-.27-1.17-.27-1.17-.1-.13-.25-.11-.39.03-.26.25-.39.55-.33.93.04.28.03.19.06.43.02.12.05.35.05.42,0,.06-.02.12-.06.16-.13.14-.34,0-.5-.07-3.12-1.29-5.79-3.1-8.12-5.4-.92-.94-2.48-2.83-2.48-2.83h0c-.06-.07-.13-.11-.24-.06-.2.09-.2.35-.22.57-.04.44.2.76.45,1.06,3.52,4.11,7.82,7.06,13,8.54l.93.25s.02,0,.03,0c0,0,0,0,0,0l.8.21h.02s-.06.02-.09.03c-.88.17-2.63-.15-4.04-.47-.7-.15-1.38-.32-2.05-.53-.03,0-.05-.01-.05-.01h0c-2.6-.83-4.97-2.17-7.05-4.21-.2-.19-.38-.4-.56-.61-.16-.2-.5-.61-.62-.75-.01-.02-.03-.03-.04-.05h0s0,0,0,0c-.04-.04-.08-.06-.14-.05-.14.03-.19.18-.21.31-.11.51.05.93.39,1.31,2.13,2.39,4.8,3.91,7.81,4.91,1.71.57,3.46.91,5.25,1.13-1.07.35-2.16.45-3.27.42-2.14-.05-4.15-.57-6.04-1.49-.7-.36-1.34-.76-1.52-.86-.1-.07-.2-.11-.3-.03-.19.15-.06.4,0,.6.12.38.38.65.73.83,3.08,1.63,6.32,2.28,9.78,1.7.35-.06.68-.12,1.05-.25-.58.5-1.25.83-1.95,1.08-2.17.79-4.32.76-6.46.22-.69-.18-1.49-.48-1.49-.48h0c-.12-.05-.23-.07-.32.06-.16.22-.02.46.12.67.32.5.94.55,1.43.72.17.05-.29.36-.43.47-.71.54-1.4,1.04-2.11,1.56l-.46.34h0c-.08.05-.15.12-.11.23.04.13.18.15.31.18.51.1.94-.06,1.33-.36.86-.64,1.73-1.26,2.56-1.93.32-.25.61-.23,1.04-.12-1.09.82-2.11,1.59-3.13,2.36-1.03.78-2.07,1.56-3.1,2.33l-.68.51s-.02.01-.03.02h-.01s0,0,0,0c-.06.05-.1.1-.09.17.03.15.21.19.36.22.61.11.99-.12,1.41-.44,2.09-1.57,4.19-3.13,6.27-4.71.47-.36.95-.56,1.62-.48-.57.43-1.12.84-1.67,1.25l-1.47,1.09s-.02.02-.03.02h0c-.08.06-.13.13-.1.24.06.19.29.22.49.25.37.05.68-.08.96-.29.17-.12.33-.24.5-.37h0s2.25-1.79,2.25-1.79c0,0,.53-.38,1.15-.73.05-.03.11-.07.17-.1.02-.01.08-.04.17-.08.07-.03.13-.07.2-.1.1-.05.21-.11.33-.18.36-.15,1.4-.64,2.26-1.55.41-.32.98-.73,1.43-.92h0s0,0,0,0c.03-.02.07-.03.1-.04h0s0,0,0,0c.02,0,.04-.02.06-.02l.06-.02c.16-.05.43-.06.46.29,0,.09,0,.18,0,.27,0,.07-.02.15-.03.21-.01.06.01.12.06.15,0,0,.02.01.02.01.06.03.14.02.18-.03.02-.02.03-.04.05-.06.22-.23.44-.39.68-.49.35-.14.7-.14,1.07,0,.18.07.35.16.51.28.07.05.14.11.21.17.04.04.08.08.12.12h0s.03.04.03.04c0,0,.01.01.02.02.04.03.08.04.12.03.05-.01.09-.05.11-.1,0,0,0-.02.01-.03.11-.31.13-.6.07-.89Z"/> + <path style="fill: #fddab4;" d="M20.22,11.62c-.03.16-.05.31-.08.47.03-.16.06-.31.08-.47h0Z"/> + <path style="fill: #fddab4;" d="M22.99,13.35s.02,0,.03.01c-.01,0-.02,0-.03-.01"/> + <polyline style="fill: #fddab4;" points="21.68 12.5 21.68 12.5 21.68 12.5"/> + <polyline style="fill: #fddab4;" points="21.73 12.39 21.73 12.39 21.73 12.39"/> + <polyline style="fill: #fddab4;" points="21.74 12.37 21.73 12.38 21.74 12.37"/> + <polyline style="fill: #fddab4;" points="23.45 12.37 23.45 12.38 23.45 12.37"/> + <path style="fill: #fddab4;" d="M20.72,12.26s-.04.08-.06.12c.02-.04.04-.08.06-.12"/> + <polyline style="fill: #fddab4;" points="18.86 12.17 18.86 12.17 18.86 12.17"/> + <polyline style="fill: #fddab4;" points="18.58 12.04 18.58 12.04 18.58 12.04"/> + <polyline style="fill: #fddab4;" points="18.58 12.04 18.58 12.04 18.58 12.04"/> + <polyline style="fill: #fddab4;" points="18.58 12.04 18.58 12.04 18.58 12.04"/> + <polyline style="fill: #fddab4;" points="18.58 12.04 18.58 12.04 18.58 12.04"/> + <polyline style="fill: #fddab4;" points="18.58 12.03 18.58 12.04 18.58 12.03"/> + <polyline style="fill: #fddab4;" points="18.52 11.84 18.52 11.84 18.52 11.84"/> + <path style="fill: #fddab4;" d="M19.22,11.72s-.01.06-.02.09c0-.03.01-.06.02-.09"/> + <path style="fill: #fddab4;" d="M17.51,11.51s-.04.04-.07.05c.02,0,.05-.02.07-.05"/> + <path style="fill: #e5b3a5;" d="M18.58,12.04s.04.04.07.06h0s-.05-.04-.07-.06M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.57,12.03s0,0,0,0c0,0,0,0,0,0M18.52,11.84s0,0,0,0c0,0,0,0,0,0M18.52,11.84h0s0,0,0,0c0,0,0,0,0,0M17.17,11.51s0,0,0,0c.06.03.13.05.19.05.03,0,.05,0,.07-.01-.02,0-.05.01-.07.01-.06,0-.13-.02-.19-.05M18.52,11.31s0,0,0,0c0,0,0,0,0,0"/> + <path style="fill: #e5b3a5;" d="M23.02,13.37s.01,0,.02,0h0s-.01,0-.02,0M21.78,12.86s0,0,0,0c0,0,0,0,0,0M21.59,12.76s.02.08.06.11c.02.01.03.02.05.02.03,0,.05-.01.08-.03-.03.02-.05.03-.08.03-.02,0-.03,0-.05-.02-.04-.03-.06-.07-.06-.11M21.68,12.5s0,.01,0,.02c0,0,0-.01,0-.02M21.73,12.39s-.03.07-.04.11c.01-.03.03-.07.04-.11M21.73,12.38s0,0,0,.01c0,0,0,0,0-.01M23.45,12.38s0,0,0,0c0,0,0,0,0,0M23.45,12.37s0,0,0,0c0,0,0,0,0,0M21.74,12.37s0,0,0,0c0,0,0,0,0,0M20.1,12.32c0,.1.04.19.19.24.05.02.09.02.12.02.13,0,.19-.09.24-.2-.05.1-.11.2-.24.2-.04,0-.08,0-.12-.02-.15-.05-.19-.14-.19-.24M18.86,12.17h0,0M19.11,12.07c-.05.06-.11.1-.22.1-.01,0-.02,0-.03,0,.01,0,.02,0,.03,0,.1,0,.17-.04.22-.1M21.08,11.32s0,0,0,0c-.05.15-.09.3-.15.44-.06.17-.14.34-.21.5h0c.08-.16.15-.33.21-.5.05-.15.1-.3.15-.45M19.3,11.23c-.02.16-.05.33-.08.49.03-.16.06-.33.08-.49h0M23.28,10.45s0,0,0,0c-.22.73-.57,1.42-1.12,2.04-.07.07-.14.14-.21.21h0c.07-.07.14-.14.21-.21.55-.62.9-1.31,1.12-2.04"/> + <path style="fill: url(#linear-gradient-2); opacity: .4;" d="M6.25,3.13s-.06,0-.09.02c-.14.06-.18.21-.2.37.78,1.02,3.84,4.77,8.67,7.35,0,0,4.72,2.49,9.48,2.76-.37-.05-.7-.13-1-.25-.02,0-.04-.01-.05-.02h0c-1.73-.37-3.2-.84-4.24-1.21.02,0,.04,0,.06,0-.02,0-.04,0-.06,0-.06-.01-.11-.03-.15-.05-.82-.3-1.35-.53-1.47-.59-.06-.03-.12-.06-.17-.08-3.12-1.29-5.79-3.1-8.12-5.4-.92-.94-2.48-2.83-2.48-2.83h0s-.09-.08-.15-.08"/> + <path style="fill: url(#linear-gradient-3); opacity: .4;" d="M6.59,7.12s-.02,0-.03,0c-.14.03-.19.18-.21.31,0,.02,0,.05-.01.07,2.41,3.27,5.65,4.6,5.65,4.6,3.23,1.52,5.88,1.83,7.5,1.83.67,0,1.16-.05,1.44-.09-.13.01-.27.02-.42.02-.95,0-2.3-.26-3.43-.52-.7-.15-1.38-.32-2.05-.53-.03,0-.05-.01-.05-.01h0c-2.6-.83-4.97-2.17-7.05-4.21-.2-.19-.38-.4-.56-.61-.16-.2-.5-.61-.62-.75-.01-.02-.03-.03-.04-.05h0s0,0,0,0c-.03-.03-.06-.05-.1-.05"/> + <path style="fill: url(#linear-gradient-4); opacity: .4;" d="M8.78,12.8s-.08.01-.12.04c-.16.13-.09.34-.02.52,1.21.73,3.98,2.16,7.27,2.16,1.24,0,2.55-.2,3.88-.72-.96.31-1.94.43-2.94.43-.11,0-.22,0-.33,0-2.14-.05-4.15-.57-6.04-1.49-.7-.36-1.34-.76-1.52-.86-.06-.04-.12-.07-.18-.07"/> + <path style="fill: url(#linear-gradient-5); opacity: .4;" d="M20.02,15.9c-.53.4-1.12.68-1.74.91-1.16.42-2.32.61-3.47.61-1,0-1.99-.14-2.99-.39-.69-.18-1.49-.48-1.49-.48h0c-.05-.02-.1-.04-.15-.04-.06,0-.12.03-.17.1-.07.1-.08.21-.06.32.76.35,2.4.98,4.39.98,1.73,0,3.73-.47,5.67-2.01"/> + <path style="fill: url(#linear-gradient-6); opacity: .4;" d="M14.87,18.37c-1.87,0-3.29-.38-3.47-.43.05.02.1.03.15.05.17.05-.29.36-.43.47-.52.4-1.04.78-1.56,1.16h1.59c.5-.37,1-.74,1.49-1.13.18-.14.35-.2.55-.2.15,0,.31.03.49.08-1.09.82-2.11,1.59-3.13,2.36-.6.45-1.19.9-1.79,1.34h1.6c1.44-1.08,2.88-2.15,4.31-3.24.33-.25.67-.42,1.07-.48-.3.02-.59.03-.88.03Z"/> + <path style="fill: url(#Fade_to_Black_2); opacity: .4;" d="M14.54,19.66h1.55l1.14-.91s.53-.38,1.15-.73c.05-.03.11-.07.17-.1.02-.01.08-.04.17-.08.07-.03.13-.07.2-.1.1-.05.22-.11.35-.19-1.1.46-2.23.69-3.28.77.01,0,.03,0,.04,0,.09,0,.18,0,.27.02-.57.43-1.12.84-1.67,1.25l-.09.07Z"/> + <path style="fill: url(#linear-gradient-7); opacity: .4;" d="M18.52,11.84c0-.14,0-.28,0-.42h0s0-.01,0-.01c0-.02,0-.04,0-.06h0s0-.03,0-.03c0-.12-.01-.23-.03-.35-.37-.16-.72-.35-1.04-.59,0,.03,0,.07,0,.1.04.28.03.19.06.43.02.12.05.35.05.42,0,.06-.02.12-.06.16-.04.04-.09.06-.14.06-.06,0-.13-.02-.19-.05.12.06.65.29,1.48.59-.09-.05-.12-.14-.12-.27Z"/> + <path style="fill: url(#linear-gradient-8); opacity: .4;" d="M24.54,9.21c-.34.48-.77.9-1.26,1.24-.22.73-.57,1.42-1.12,2.04-.07.07-.14.14-.21.21-.07.07-.17.19-.25.19-.02,0-.03,0-.05-.02-.13-.09-.01-.27.03-.36.21-.48.37-.98.48-1.47-.35.13-.71.22-1.08.27-.05.15-.09.3-.15.44-.06.17-.14.34-.21.5-.07.14-.12.32-.3.32-.04,0-.08,0-.12-.02-.26-.09-.19-.29-.16-.47.05-.24.09-.49.12-.73-.33-.01-.65-.05-.96-.12-.03.19-.06.39-.1.58-.02.13-.08.35-.31.35-.03,0-.06,0-.09-.01,1.04.37,2.51.84,4.24,1.21h0s-.03-.01-.05-.02c-.28-.12.22-.61.45-.96.64-.98.95-2.06,1.1-3.18Z"/> + <path style="fill: url(#Fade_to_Black_2-2); opacity: .5;" d="M24.28,14.56c-.22,0-.43.02-.65.06-.04,0-.08.01-.12.01-.06,0-.12,0-.17-.03,0,.01.02.02.03.03.09.13.14.27.16.42.02.03.04.06.06.1h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0t0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,.07,0,.1c0,.05,0,.11,0,.17,0,.07-.02.15-.03.21,0,0,0,.02,0,.03,0,.05.02.09.06.12h.02s.05.03.07.03c.04,0,.08-.02.11-.05.02-.02.03-.04.05-.06.22-.23.44-.39.68-.49.17-.07.35-.11.53-.11s.36.04.55.11c.18.07.35.16.51.28.07.05.14.11.21.17.04.04.08.08.12.12h0s.03.04.03.04l.02.02s.06.03.09.03c-.15-.32-.33-.61-.6-.84-.5-.43-1.13-.62-1.77-.62"/> + <path style="fill: url(#linear-gradient-9); opacity: .4;" d="M24.22,12.45h-.03c-.07.01-.12.07-.13.14,0,.05,0,.11,0,.17,0,.02,0,.04-.01.05.27.4.84,1.1,1.72,1.36,0,0,1.36.71,1.01,1.74h0v-.03c.12-.31.14-.6.08-.89-.06-.28-.2-.53-.42-.76-.08-.09-.18-.17-.28-.25-.05-.04-.1-.07-.15-.1-.07-.04-.14-.08-.21-.12-.28-.14-.51-.29-.72-.44-.08-.06-.17-.13-.24-.19-.19-.16-.31-.3-.4-.45-.03-.05-.06-.1-.08-.15-.01-.02-.03-.04-.05-.05-.03-.02-.06-.03-.09-.03"/> + <path style="fill: url(#linear-gradient-10); opacity: .5;" d="M22.5,15.32l.28-.16,1.16-1.55s-.63-.12-.94-.26c0,0-.32,1.45-1.49,2.66l.43-.32c.17-.12.33-.23.56-.37Z"/> + <g> + <path style="fill: #404041;" d="M38,7.1h2.95c2.13,0,3.11,1.1,3.11,3.35v1.12c0,2.25-.98,3.35-3.11,3.35h-1.71v6.14h-1.24V7.1ZM40.95,13.78c1.3,0,1.87-.58,1.87-2.15v-1.26c0-1.55-.58-2.15-1.87-2.15h-1.71v5.56h1.71Z"/> + <path style="fill: #404041;" d="M48.61,7.1h1.24v6.12h3.81v-6.12h1.24v13.95h-1.24v-6.72h-3.81v6.72h-1.24V7.1Z"/> + <path style="fill: #404041;" d="M59.73,17.84v-7.54c0-2.21,1.12-3.41,3.13-3.41s3.13,1.2,3.13,3.41v7.54c0,2.21-1.12,3.41-3.13,3.41s-3.13-1.2-3.13-3.41ZM64.75,17.92v-7.69c0-1.5-.68-2.21-1.89-2.21s-1.89.72-1.89,2.21v7.69c0,1.5.68,2.19,1.89,2.19s1.89-.7,1.89-2.19Z"/> + <path style="fill: #404041;" d="M70.85,7.1h5.58v1.12h-4.35v5h3.57v1.12h-3.57v5.58h4.35v1.14h-5.58V7.1Z"/> + <path style="fill: #404041;" d="M80.9,7.1h1.63l3.63,10.78V7.1h1.16v13.95h-1.32l-3.97-11.9v11.9h-1.14V7.1Z"/> + <path style="fill: #404041;" d="M92.32,7.1h1.24v13.95h-1.24V7.1Z"/> + <path style="fill: #404041;" d="M100.79,13.94l-2.73-6.84h1.32l2.19,5.54,2.21-5.54h1.2l-2.73,6.84,2.85,7.12h-1.32l-2.31-5.86-2.33,5.86h-1.2l2.85-7.12Z"/> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/web/app/components/base/icons/assets/public/tracing/phoenix-icon.svg b/web/app/components/base/icons/assets/public/tracing/phoenix-icon.svg new file mode 100644 index 0000000000..b30edd6c97 --- /dev/null +++ b/web/app/components/base/icons/assets/public/tracing/phoenix-icon.svg @@ -0,0 +1,97 @@ +<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 74 16" width="74" height="16"> + <defs> + <linearGradient id="linear-gradient" x1="6.05" y1=".98" x2="13.18" y2="13.34" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#11bab5"/> + <stop offset=".5" stop-color="#00adee"/> + <stop offset="1" stop-color="#0094c5"/> + </linearGradient> + <linearGradient id="linear-gradient-2" x1="12.99" y1="10.87" x2="6.97" y2=".45" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#fcfdff" stop-opacity="0"/> + <stop offset=".12" stop-color="#fcfdff" stop-opacity=".17"/> + <stop offset=".3" stop-color="#fcfdff" stop-opacity=".42"/> + <stop offset=".47" stop-color="#fcfdff" stop-opacity=".63"/> + <stop offset=".64" stop-color="#fcfdff" stop-opacity=".79"/> + <stop offset=".78" stop-color="#fcfdff" stop-opacity=".9"/> + <stop offset=".91" stop-color="#fcfdff" stop-opacity=".97"/> + <stop offset="1" stop-color="#fcfdff"/> + </linearGradient> + <linearGradient id="linear-gradient-3" x1="11.21" y1="10.8" x2="6.88" y2="3.29" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-4" x1="10.62" y1="11.35" x2="8.2" y2="7.16" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-5" x1="10.72" y1="12.2" x2="9.21" y2="9.59" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-6" x1="8.17" y1="11.96" x2="8.17" y2="14.71" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".03" stop-color="#231f20" stop-opacity=".9"/> + <stop offset=".22" stop-color="#231f20" stop-opacity=".4"/> + <stop offset=".42" stop-color="#231f20" stop-opacity=".1"/> + <stop offset=".65" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="Fade_to_Black_2" data-name="Fade to Black 2" x1="11.26" y1="11.7" x2="11.26" y2="13.1" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset="1" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="linear-gradient-7" x1="11.94" y1="8.07" x2="11.94" y2="6.92" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".03" stop-color="#231f20" stop-opacity=".95"/> + <stop offset=".51" stop-color="#231f20" stop-opacity=".26"/> + <stop offset=".81" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="linear-gradient-8" x1="14.45" y1="8.92" x2="14.45" y2="6.14" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".18" stop-color="#231f20" stop-opacity=".48"/> + <stop offset=".38" stop-color="#231f20" stop-opacity=".12"/> + <stop offset=".58" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + <linearGradient id="Fade_to_Black_2-2" data-name="Fade to Black 2" x1="17.03" y1="11.1" x2="16.07" y2="9.44" xlink:href="#Fade_to_Black_2"/> + <linearGradient id="linear-gradient-9" x1="17.8" y1="10.64" x2="16.36" y2="8.16" xlink:href="#linear-gradient-2"/> + <linearGradient id="linear-gradient-10" x1="14.07" y1="10.22" x2="16.16" y2="9.02" gradientUnits="userSpaceOnUse"> + <stop offset="0" stop-color="#231f20"/> + <stop offset=".31" stop-color="#231f20" stop-opacity=".5"/> + <stop offset=".67" stop-color="#231f20" stop-opacity=".13"/> + <stop offset="1" stop-color="#231f20" stop-opacity="0"/> + </linearGradient> + </defs> + <g id="Layer_1-2" data-name="Layer 1"> + <rect style="fill: none;" y=".02" width="74" height="16"/> + <g id="Phoenix_horiz_-_gradient" data-name="Phoenix horiz - gradient"> + <path style="fill: url(#linear-gradient);" d="M17.91,10c-.04-.19-.14-.36-.28-.51-.06-.06-.12-.11-.19-.16-.03-.02-.07-.05-.1-.07-.04-.03-.09-.05-.14-.08-.19-.09-.34-.19-.48-.29-.06-.04-.11-.09-.16-.13-.12-.11-.21-.2-.27-.3-.02-.03-.04-.07-.05-.1,0-.01-.02-.03-.03-.04-.02-.02-.05-.02-.08-.02-.04,0-.08.04-.08.09,0,.03,0,.07,0,.11,0,.02,0,.04-.03.05,0,0,0,0-.02,0-.03,0-.06.02-.07.04-.02.02-.02.05-.02.08,0,.03.01.04.02.06h0s0,.03.01.04c.01.03.03.05.05.08.03.05.07.09.11.15.01.02.02.04,0,.06-.01.02-.03.03-.05.03-.25-.03-.46-.09-.67-.16-.02,0-.05-.02-.07-.03-.18-.08.15-.41.3-.64.66-1.02.8-2.19.81-3.38,0-1.15-.12-2.27-.35-3.39h0c-.15-.94-.27-1.29-.35-1.42-.02-.03-.04-.06-.06-.07-.02-.01-.03,0-.03,0-.04,0-.09.04-.15.1-.25.26-.23.57-.17.89.31,1.47.55,2.95.44,4.47-.07,1.04-.27,2.05-1,2.86-.04.05-.09.1-.14.14-.05.05-.14.16-.2.11-.08-.06,0-.18.02-.24.43-1,.55-2.05.48-3.12,0-.02,0-.05,0-.07h0s0-.04,0-.06c-.01-.14-.02-.28-.04-.43-.18-1.71-.4-1.82-.4-1.82h0s-.03-.02-.06-.03c-.12-.02-.18.08-.23.15-.23.35-.13.72-.05,1.08.26,1.28.27,2.55-.18,3.79-.04.11-.09.22-.14.33-.05.12-.09.26-.28.2-.18-.06-.13-.19-.1-.31.06-.31.11-.62.14-.93h0s0-.06,0-.09c0-.05,0-.11.01-.16,0-.05,0-.09,0-.13,0-.02,0-.04,0-.07.04-.99-.13-1.44-.19-1.57,0-.02-.01-.03-.02-.04h0s0,0,0,0c-.07-.1-.17-.09-.27,0-.17.17-.27.37-.22.62.13.74.05,1.46-.08,2.19-.02.1-.06.27-.27.23-.14-.03-.17-.09-.18-.21,0-.09,0-.19,0-.28,0,0,0,0,0,0h0s0-.03,0-.04h0s0-.01,0-.02c0-.13-.02-.25-.03-.38,0-.01,0-.02,0-.04-.08-.71-.18-.78-.18-.78-.07-.09-.17-.07-.26.02-.17.17-.26.37-.22.62.03.19.02.13.04.29.01.08.03.23.03.28,0,.04-.01.08-.04.11-.09.09-.23,0-.34-.05-2.08-.86-3.86-2.07-5.42-3.6-.61-.62-1.65-1.88-1.65-1.88h0s-.09-.07-.16-.04c-.14.06-.13.23-.14.38-.02.29.13.51.3.7,2.35,2.74,5.21,4.71,8.67,5.69l.62.16s.01,0,.02,0c0,0,0,0,0,0l.54.14h.01s-.04.01-.06.02c-.59.12-1.76-.1-2.69-.32-.46-.1-.92-.22-1.36-.36-.02,0-.03,0-.03,0h0c-1.73-.55-3.32-1.44-4.7-2.81-.13-.13-.25-.27-.37-.41-.11-.13-.34-.4-.41-.5,0-.01-.02-.02-.03-.03h0s0,0,0,0c-.02-.02-.05-.04-.09-.03-.1.02-.12.12-.14.21-.07.34.04.62.26.88,1.42,1.59,3.2,2.61,5.21,3.28,1.14.38,2.31.61,3.5.76-.71.23-1.44.3-2.18.28-1.43-.04-2.77-.38-4.03-.99-.46-.24-.9-.5-1.01-.58-.07-.04-.13-.07-.2-.02-.13.1-.04.27,0,.4.08.26.25.43.49.55,2.05,1.08,4.22,1.52,6.52,1.13.23-.04.45-.08.7-.16-.39.33-.83.55-1.3.72-1.44.53-2.88.51-4.31.15-.46-.12-.99-.32-.99-.32h0c-.08-.03-.15-.05-.21.04-.1.15-.01.3.08.45.21.33.63.36.95.48.11.04-.19.24-.29.31-.47.36-.94.69-1.41,1.04l-.31.23h0c-.05.04-.1.08-.08.15.03.09.12.1.2.12.34.07.62-.04.89-.24.57-.43,1.15-.84,1.71-1.28.21-.17.4-.15.7-.08-.73.55-1.4,1.06-2.08,1.57-.69.52-1.38,1.04-2.07,1.56l-.46.34s-.01,0-.02.01h0s0,0,0,0c-.04.03-.07.07-.06.11.02.1.14.13.24.15.41.07.66-.08.94-.29,1.39-1.05,2.79-2.09,4.18-3.14.31-.24.63-.37,1.08-.32-.38.29-.75.56-1.11.83l-.98.73s-.01.01-.02.02h0c-.05.04-.09.09-.07.16.04.13.19.15.33.17.24.04.45-.05.64-.19.11-.08.22-.16.33-.24h0s1.5-1.19,1.5-1.19c0,0,.35-.25.76-.49.04-.02.07-.05.11-.07.02,0,.05-.02.11-.05.04-.02.09-.05.13-.07.06-.03.14-.07.22-.12.24-.1.93-.42,1.51-1.03.27-.21.66-.48.95-.62h0s0,0,0,0c.02-.01.04-.02.07-.03h0s0,0,0,0c.01,0,.03-.01.04-.01h.04c.11-.05.28-.06.31.18,0,.06,0,.12,0,.18,0,.05-.01.1-.02.14,0,.04,0,.08.04.1,0,0,.01,0,.02,0,.04.02.09.01.12-.02.01-.01.02-.03.04-.04.15-.15.3-.26.45-.33.23-.1.47-.1.72,0,.12.05.23.11.34.19.05.03.1.07.14.12.03.03.06.05.08.08h0s.02.02.02.02c0,0,0,0,.01.01.02.02.05.02.08.02.03,0,.06-.03.07-.06,0,0,0-.01,0-.02.07-.21.09-.4.04-.59Z"/> + <path style="fill: #fddab4;" d="M13.48,7.75c-.02.1-.03.21-.05.31.02-.1.04-.21.05-.31h0Z"/> + <path style="fill: #fddab4;" d="M15.33,8.9s.02,0,.02,0c0,0-.02,0-.02,0"/> + <polyline style="fill: #fddab4;" points="14.46 8.33 14.46 8.33 14.46 8.33"/> + <polyline style="fill: #fddab4;" points="14.48 8.26 14.48 8.26 14.48 8.26"/> + <polyline style="fill: #fddab4;" points="14.49 8.25 14.49 8.25 14.49 8.25"/> + <polyline style="fill: #fddab4;" points="15.64 8.25 15.63 8.25 15.64 8.25"/> + <path style="fill: #fddab4;" d="M13.81,8.17s-.02.06-.04.08c.01-.03.02-.06.04-.08"/> + <polyline style="fill: #fddab4;" points="12.57 8.11 12.57 8.11 12.57 8.11"/> + <polyline style="fill: #fddab4;" points="12.39 8.03 12.39 8.03 12.39 8.03"/> + <polyline style="fill: #fddab4;" points="12.39 8.03 12.39 8.03 12.39 8.03"/> + <polyline style="fill: #fddab4;" points="12.39 8.03 12.39 8.03 12.39 8.03"/> + <polyline style="fill: #fddab4;" points="12.38 8.02 12.38 8.03 12.38 8.02"/> + <polyline style="fill: #fddab4;" points="12.38 8.02 12.38 8.02 12.38 8.02"/> + <polyline style="fill: #fddab4;" points="12.35 7.89 12.35 7.89 12.35 7.89"/> + <path style="fill: #fddab4;" d="M12.81,7.81s0,.04-.01.06c0-.02,0-.04.01-.06"/> + <path style="fill: #fddab4;" d="M11.67,7.67s-.03.02-.05.03c.02,0,.03-.02.05-.03"/> + <path style="fill: #e5b3a5;" d="M12.39,8.03s.03.03.05.04h0s-.03-.03-.05-.04M12.39,8.03s0,0,0,0c0,0,0,0,0,0M12.39,8.03s0,0,0,0c0,0,0,0,0,0M12.38,8.03s0,0,0,0c0,0,0,0,0,0M12.38,8.02s0,0,0,0c0,0,0,0,0,0M12.38,8.02s0,0,0,0c0,0,0,0,0,0M12.35,7.89s0,0,0,0c0,0,0,0,0,0M12.35,7.89h0s0,0,0,0c0,0,0,0,0,0M11.45,7.68s0,0,0,0c.04.02.09.03.13.03.02,0,.03,0,.05,0-.02,0-.03,0-.05,0-.04,0-.09-.02-.13-.03M12.34,7.54s0,0,0,0c0,0,0,0,0,0"/> + <path style="fill: #e5b3a5;" d="M15.35,8.91s0,0,.01,0h0s0,0-.01,0M14.52,8.58s0,0,0,0c0,0,0,0,0,0M14.39,8.51s.01.06.04.08c.01,0,.02.01.03.01.02,0,.04,0,.05-.02-.02.01-.04.02-.05.02-.01,0-.02,0-.03-.01-.03-.02-.04-.05-.04-.08M14.46,8.33s0,0,0,.01c0,0,0,0,0-.01M14.48,8.26s-.02.05-.03.07c0-.02.02-.05.03-.07M14.49,8.25s0,0,0,0c0,0,0,0,0,0M15.63,8.25s0,0,0,0c0,0,0,0,0,0M15.64,8.24s0,0,0,0c0,0,0,0,0,0M14.49,8.24s0,0,0,0c0,0,0,0,0,0M13.4,8.21c0,.07.03.13.13.16.03.01.06.01.08.01.09,0,.13-.06.16-.13-.03.07-.07.13-.16.13-.02,0-.05,0-.08-.01-.1-.03-.13-.09-.13-.16M12.57,8.11h0,0M12.74,8.04s-.08.07-.14.07c0,0-.01,0-.02,0,0,0,.01,0,.02,0,.07,0,.11-.03.14-.07M14.05,7.54s0,0,0,0c-.03.1-.06.2-.1.3-.04.11-.09.22-.14.33h0c.05-.11.1-.22.14-.33.04-.1.07-.2.1-.3M12.87,7.49c-.02.11-.03.22-.05.33.02-.11.04-.22.06-.33h0M15.52,6.97s0,0,0,0c-.15.49-.38.95-.75,1.36-.04.05-.09.1-.14.14h0s.09-.09.14-.14c.37-.41.6-.87.75-1.36"/> + <path style="fill: url(#linear-gradient-2); opacity: .4;" d="M4.17,2.09s-.04,0-.06.01c-.1.04-.12.14-.13.25.52.68,2.56,3.18,5.78,4.9,0,0,3.15,1.66,6.32,1.84-.25-.03-.46-.09-.67-.16-.01,0-.02,0-.04-.01h0c-1.15-.25-2.13-.56-2.83-.81.01,0,.03,0,.04,0-.01,0-.03,0-.04,0-.04,0-.07-.02-.1-.04-.55-.2-.9-.35-.98-.39-.04-.02-.08-.04-.12-.05-2.08-.86-3.86-2.07-5.42-3.6-.61-.62-1.65-1.88-1.65-1.88h0s-.06-.05-.1-.05"/> + <path style="fill: url(#linear-gradient-3); opacity: .4;" d="M4.39,4.75s-.01,0-.02,0c-.1.02-.12.12-.14.21,0,.02,0,.03,0,.05,1.61,2.18,3.77,3.07,3.77,3.07,2.15,1.01,3.92,1.22,5,1.22.45,0,.77-.04.96-.06-.09,0-.18.01-.28.01-.63,0-1.53-.17-2.29-.35-.46-.1-.92-.22-1.36-.36-.02,0-.03,0-.03,0h0c-1.73-.55-3.32-1.44-4.7-2.81-.13-.13-.25-.27-.37-.41-.11-.13-.34-.4-.41-.5,0-.01-.02-.02-.03-.03h0s0,0,0,0c-.02-.02-.04-.03-.07-.03"/> + <path style="fill: url(#linear-gradient-4); opacity: .4" d="M5.86,8.53s-.05,0-.08.03c-.11.08-.06.22-.02.35.81.49,2.66,1.44,4.84,1.44.83,0,1.7-.14,2.59-.48-.64.21-1.3.28-1.96.28-.07,0-.15,0-.22,0-1.43-.04-2.77-.38-4.03-.99-.46-.24-.9-.5-1.01-.58-.04-.03-.08-.05-.12-.05"/> + <path style="fill: url(#linear-gradient-5); opacity: .4;" d="M13.34,10.6c-.35.27-.75.46-1.16.61-.77.28-1.55.41-2.32.41-.67,0-1.33-.09-1.99-.26-.46-.12-.99-.32-.99-.32h0s-.07-.03-.1-.03c-.04,0-.08.02-.11.06-.05.07-.05.14-.04.21.51.24,1.6.66,2.93.66,1.15,0,2.49-.32,3.78-1.34"/> + <path style="fill: url(#linear-gradient-6); opacity: .4;" d="M9.91,12.25c-1.25,0-2.19-.25-2.31-.29.04.01.07.02.1.03.11.04-.19.24-.29.31-.35.27-.69.52-1.04.77h1.06c.33-.25.67-.5.99-.75.12-.1.24-.13.37-.13.1,0,.2.02.33.05-.73.55-1.4,1.06-2.08,1.57-.4.3-.79.6-1.19.9h1.07c.96-.72,1.92-1.44,2.87-2.16.22-.17.44-.28.71-.32-.2.01-.4.02-.58.02Z"/> + <path style="fill: url(#Fade_to_Black_2); opacity: .4;" d="M9.69,13.1h1.03l.76-.6s.35-.25.76-.49c.04-.02.07-.05.11-.07.02,0,.05-.02.11-.05.04-.02.09-.05.13-.07.07-.03.15-.08.23-.12-.73.31-1.49.46-2.18.52,0,0,.02,0,.03,0,.06,0,.12,0,.18.01-.38.29-.75.56-1.11.83l-.06.05Z"/> + <path style="fill: url(#linear-gradient-7); opacity: .4;" d="M12.35,7.89c0-.09,0-.19,0-.28h0s0,0,0,0c0-.01,0-.03,0-.04h0s0-.02,0-.02c0-.08,0-.16-.02-.23-.25-.1-.48-.24-.69-.39,0,.02,0,.04,0,.07.03.19.02.13.04.29.01.08.03.23.03.28,0,.04-.01.08-.04.11-.03.03-.06.04-.09.04-.04,0-.09-.02-.13-.03.08.04.43.19.98.39-.06-.04-.08-.09-.08-.18Z"/> + <path style="fill: url(#linear-gradient-8); opacity: .4;" d="M16.36,6.14c-.23.32-.51.6-.84.83-.15.49-.38.95-.75,1.36-.04.05-.09.1-.14.14-.05.04-.11.12-.17.12-.01,0-.02,0-.03-.01-.08-.06,0-.18.02-.24.14-.32.24-.65.32-.98-.23.09-.47.15-.72.18-.03.1-.06.2-.1.3-.04.11-.09.22-.14.33-.05.1-.08.21-.2.21-.02,0-.05,0-.08-.01-.18-.06-.13-.19-.1-.31.03-.16.06-.32.08-.49-.22,0-.43-.04-.64-.08-.02.13-.04.26-.07.39-.02.09-.05.24-.21.24-.02,0-.04,0-.06,0,.69.25,1.68.56,2.83.81h0s-.02,0-.03-.01c-.18-.08.15-.41.3-.64.43-.66.64-1.37.73-2.12Z"/> + <path style="fill: url(#Fade_to_Black_2-2); opacity: .5;" d="M16.19,9.7c-.14,0-.29.01-.43.04-.03,0-.05,0-.08,0-.04,0-.08,0-.11-.02,0,0,.01.01.02.02.06.09.1.18.11.28.02.02.03.04.04.07h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0t0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,.05,0,.07c0,.04,0,.07,0,.11,0,.05-.01.1-.02.14,0,0,0,.01,0,.02,0,.03.02.06.04.08h.02s.03.02.05.02c.03,0,.06-.01.08-.03.01-.01.02-.03.04-.04.15-.15.3-.26.45-.33.12-.05.23-.07.35-.07s.24.02.36.07c.12.05.23.11.34.19.05.03.1.07.14.12.03.03.06.05.08.08h0s.02.02.02.02h.01s.04.03.06.03c-.1-.21-.22-.41-.4-.56-.33-.29-.75-.41-1.18-.41"/> + <path style="fill: url(#linear-gradient-9); opacity: .4;" d="M16.15,8.3h-.02s-.08.05-.08.09c0,.03,0,.07,0,.11,0,.01,0,.02,0,.03.18.26.56.73,1.15.91,0,0,.91.47.68,1.16h0v-.02c.08-.21.09-.4.05-.59-.04-.19-.14-.36-.28-.51-.06-.06-.12-.11-.19-.16-.03-.02-.07-.05-.1-.07-.04-.03-.09-.05-.14-.08-.19-.09-.34-.19-.48-.29-.06-.04-.11-.09-.16-.13-.12-.11-.21-.2-.27-.3-.02-.03-.04-.07-.05-.1,0-.01-.02-.03-.03-.04-.02-.01-.04-.02-.06-.02"/> + <path style="fill: url(#linear-gradient-10); opacity: .5;" d="M15,10.21l.18-.11.77-1.03s-.42-.08-.63-.17c0,0-.21.96-.99,1.77l.29-.21c.11-.08.22-.15.38-.25Z"/> + <g> + <path style="fill: #404041;" d="M25.33,4.73h1.97c1.42,0,2.07.73,2.07,2.23v.74c0,1.5-.65,2.23-2.07,2.23h-1.14v4.09h-.82V4.73ZM27.3,9.19c.86,0,1.25-.39,1.25-1.44v-.84c0-1.04-.39-1.44-1.25-1.44h-1.14v3.71h1.14Z"/> + <path style="fill: #404041;" d="M32.4,4.73h.82v4.08h2.54v-4.08h.82v9.3h-.82v-4.48h-2.54v4.48h-.82V4.73Z"/> + <path style="fill: #404041;" d="M39.82,11.9v-5.02c0-1.48.74-2.27,2.09-2.27s2.09.8,2.09,2.27v5.02c0,1.48-.74,2.27-2.09,2.27s-2.09-.8-2.09-2.27ZM43.17,11.95v-5.13c0-1-.45-1.48-1.26-1.48s-1.26.48-1.26,1.48v5.13c0,1,.45,1.46,1.26,1.46s1.26-.47,1.26-1.46Z"/> + <path style="fill: #404041;" d="M47.23,4.73h3.72v.74h-2.9v3.34h2.38v.74h-2.38v3.72h2.9v.76h-3.72V4.73Z"/> + <path style="fill: #404041;" d="M53.93,4.73h1.09l2.42,7.19v-7.19h.77v9.3h-.88l-2.64-7.93v7.93h-.76V4.73Z"/> + <path style="fill: #404041;" d="M61.55,4.73h.82v9.3h-.82V4.73Z"/> + <path style="fill: #404041;" d="M67.19,9.29l-1.82-4.56h.88l1.46,3.69,1.48-3.69h.8l-1.82,4.56,1.9,4.74h-.88l-1.54-3.91-1.55,3.91h-.8l1.9-4.74Z"/> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/web/app/components/base/icons/assets/vender/line/others/search-menu.svg b/web/app/components/base/icons/assets/vender/line/others/search-menu.svg new file mode 100644 index 0000000000..f61f69f4ba --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/others/search-menu.svg @@ -0,0 +1,7 @@ +<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.00488 16H6.67155" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.00488 9.33334H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.00488 22.6667H8.00488" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M26 22L29.3333 25.3333" stroke="#676F83" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/web/app/components/base/icons/assets/vender/other/mcp.svg b/web/app/components/base/icons/assets/vender/other/mcp.svg new file mode 100644 index 0000000000..7415c060dd --- /dev/null +++ b/web/app/components/base/icons/assets/vender/other/mcp.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z" fill="white"/> +<path d="M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z" fill="white"/> +</svg> diff --git a/web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg b/web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg new file mode 100644 index 0000000000..8b8729412f --- /dev/null +++ b/web/app/components/base/icons/assets/vender/other/no-tool-placeholder.svg @@ -0,0 +1,36 @@ +<svg width="204" height="36" viewBox="0 0 204 36" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.1"> +<path d="M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z" fill="#101828" fill-opacity="0.3"/> +</g> +<g opacity="0.3"> +<path d="M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z" fill="#101828" fill-opacity="0.3"/> +</g> +<g opacity="0.6"> +<path d="M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z" fill="#101828" fill-opacity="0.3"/> +</g> +<rect x="84.5" y="0.5" width="35" height="35" rx="9.5" stroke="#101828" stroke-opacity="0.04"/> +<path d="M96.167 16.3333H107.834V25.5H96.167V16.3333Z" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M100.333 19.6667H103.666" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M107.833 12.1667L105.583 15.9167" stroke="#676F83" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z" fill="#676F83"/> +<path d="M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z" fill="#676F83"/> +<g opacity="0.6" clip-path="url(#clip0_9296_51042)"> +<path d="M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z" fill="#101828" fill-opacity="0.3"/> +</g> +<g opacity="0.3"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z" fill="#101828" fill-opacity="0.3"/> +<path d="M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z" fill="#101828" fill-opacity="0.3"/> +<path d="M163.666 18H161.333V16.6667H163.666V18Z" fill="#101828" fill-opacity="0.3"/> +<path d="M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z" fill="#101828" fill-opacity="0.3"/> +<path d="M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z" fill="#101828" fill-opacity="0.3"/> +<path d="M168 13.6667H166.666V11.3333H168V13.6667Z" fill="#101828" fill-opacity="0.3"/> +</g> +<g opacity="0.1"> +<path d="M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z" fill="#101828" fill-opacity="0.3"/> +</g> +<defs> +<clipPath id="clip0_9296_51042"> +<rect width="16" height="16" fill="white" transform="translate(132 10)"/> +</clipPath> +</defs> +</svg> diff --git a/web/app/components/base/icons/assets/vender/workflow/window-cursor.svg b/web/app/components/base/icons/assets/vender/workflow/window-cursor.svg new file mode 100644 index 0000000000..af8a9bac94 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/window-cursor.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z" fill="white"/> +<path d="M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z" fill="white"/> +<path d="M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z" fill="white"/> +<path d="M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z" fill="white"/> +<path d="M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z" fill="white"/> +</svg> diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIcon.json b/web/app/components/base/icons/src/public/tracing/AliyunIcon.json new file mode 100644 index 0000000000..5cbb52c237 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/AliyunIcon.json @@ -0,0 +1,118 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "fill": "none", + "version": "1.1", + "width": "65", + "height": "16", + "viewBox": "0 0 65 16" + }, + "children": [ + { + "type": "element", + "name": "defs", + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "master_svg0_42_34281" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "x": "0", + "y": "0", + "width": "19", + "height": "16", + "rx": "0" + } + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "clip-path": "url(#master_svg0_42_34281)" + }, + "children": [ + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.06862,14.6667C3.79213,14.6667,3.45463,14.5688,3.05614,14.373C2.97908,14.3351,2.92692,14.3105,2.89968,14.2992C2.33193,14.0628,1.82911,13.7294,1.39123,13.2989C0.463742,12.3871,0,11.2874,0,10C0,8.71258,0.463742,7.61293,1.39123,6.70107C2.16172,5.94358,3.06404,5.50073,4.09819,5.37252C4.23172,3.98276,4.81755,2.77756,5.85569,1.75693C7.04708,0.585642,8.4857,0,10.1716,0C11.5256,0,12.743,0.396982,13.8239,1.19095C14.8847,1.97019,15.61,2.97855,16,4.21604L14.7045,4.61063C14.4016,3.64918,13.8374,2.86532,13.0121,2.25905C12.1719,1.64191,11.2251,1.33333,10.1716,1.33333C8.8602,1.33333,7.74124,1.7888,6.81467,2.69974C5.88811,3.61067,5.42483,4.71076,5.42483,6L5.42483,6.66667L4.74673,6.66667C3.81172,6.66667,3.01288,6.99242,2.35021,7.64393C1.68754,8.2954,1.35621,9.08076,1.35621,10C1.35621,10.9192,1.68754,11.7046,2.35021,12.3561C2.66354,12.6641,3.02298,12.9026,3.42852,13.0714C3.48193,13.0937,3.55988,13.13,3.66237,13.1803C3.87004,13.2823,4.00545,13.3333,4.06862,13.3333L4.06862,14.6667Z", + "fill-rule": "evenodd", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + }, + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M13.458613505859375,7.779393492279053C12.975613505859375,7.717463492279053,12.484813505859375,7.686503492279053,11.993983505859376,7.686503492279053C11.152583505859376,7.686503492279053,10.303403505859375,7.779393492279053,9.493183505859374,7.941943492279053C8.682953505859375,8.104503492279052,7.903893505859375,8.359943492279053,7.155983505859375,8.654083492279053C6.657383505859375,8.870823492279053,6.158783505859375,9.128843492279053,5.660181505859375,9.428153492279053C5.332974751859375,9.621673492279053,5.239486705859375,10.070633492279054,5.434253505859375,10.395743492279053L7.413073505859375,13.298533492279052C7.639003505859375,13.623603492279052,8.090863505859375,13.716463492279052,8.418073505859375,13.523003492279052C8.547913505859375,13.435263492279052,8.763453505859374,13.326893492279053,9.064693505859374,13.197863492279053C9.516553505859374,13.004333492279052,9.976203505859374,12.872733492279053,10.459223505859375,12.779863492279052C10.942243505859375,12.679263492279052,11.433053505859375,12.617333492279052,11.955023505859375,12.617333492279052L13.380683505859375,7.810353492279052L13.458613505859375,7.779393492279053ZM15.273813505859374,8.135463492279053L15.016753505859375,5.333333492279053L13.458613505859375,7.787133492279053C13.817013505859375,7.818093492279052,14.144213505859375,7.880023492279053,14.494743505859375,7.949683492279053C14.494743505859375,7.944523492279053,14.754433505859375,8.006453492279054,15.273813505859374,8.135463492279053ZM12.064083505859376,12.648273492279053L11.378523505859375,14.970463492279054L12.515943505859376,16.00003349227905L14.074083505859376,15.643933492279054L14.525943505859376,13.027603492279052C14.198743505859374,12.934663492279054,13.879283505859375,12.834063492279054,13.552083505859375,12.772133492279053C13.069083505859375,12.717933492279052,12.578283505859375,12.648273492279053,12.064083505859376,12.648273492279053ZM18.327743505859374,9.428153492279053C17.829143505859374,9.128843492279053,17.330543505859374,8.870823492279053,16.831943505859375,8.654083492279053C16.348943505859374,8.460573492279053,15.826943505859376,8.267053492279054,15.305013505859375,8.135463492279053L15.305013505859375,8.267053492279054L14.463613505859374,13.043063492279053C14.596083505859376,13.105003492279053,14.759683505859375,13.135933492279053,14.884283505859376,13.205603492279053C15.185523505859376,13.334623492279052,15.401043505859375,13.443003492279052,15.530943505859375,13.530733492279053C15.858143505859376,13.724263492279054,16.341143505859375,13.623603492279052,16.535943505859375,13.306263492279053L18.514743505859375,10.403483492279053C18.779643505859376,10.039673492279054,18.686143505859377,9.621673492279053,18.327743505859374,9.428153492279053Z", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M25.044,2.668L34.676,2.668L34.676,4.04L25.044,4.04L25.044,2.668ZM29.958,7.82Q29.258,9.066,28.355,10.41Q27.451999999999998,11.754,26.92,12.3L32.506,11.782Q31.442,10.158,30.84,9.346L32.058,8.562000000000001Q32.786,9.5,33.843,11.012Q34.9,12.524,35.516,13.546L34.214,14.526Q33.891999999999996,13.966,33.346000000000004,13.098Q32.016,13.182,29.734,13.378Q27.451999999999998,13.574,25.87,13.742L25.31,13.812L24.834,13.882L24.414,12.468Q24.708,12.37,24.862000000000002,12.265Q25.016,12.16,25.121,12.069Q25.226,11.978,25.268,11.936Q25.912,11.32,26.724,10.165Q27.536,9.01,28.208,7.82L23.854,7.82L23.854,6.434L35.866,6.434L35.866,7.82L29.958,7.82ZM42.656,7.414L42.656,8.576L41.354,8.576L41.354,1.814L42.656,1.87L42.656,7.036Q43.314,5.846,43.888000000000005,4.369Q44.462,2.892,44.714,1.6600000000000001L46.086,1.981999Q45.96,2.612,45.722,3.41L49.6,3.41L49.6,4.74L45.274,4.74Q44.616,6.56,43.706,8.128L42.656,7.414ZM38.596000000000004,2.346L39.884,2.402L39.884,8.212L38.596000000000004,8.212L38.596000000000004,2.346ZM46.184,4.964Q46.688,5.356,47.5,6.175Q48.312,6.994,48.788,7.582L47.751999999999995,8.59Q47.346000000000004,8.072,46.576,7.274Q45.806,6.476,45.204,5.902L46.184,4.964ZM48.41,9.01L48.41,12.706L49.894,12.706L49.894,13.966L37.391999999999996,13.966L37.391999999999996,12.706L38.848,12.706L38.848,9.01L48.41,9.01ZM41.676,10.256L40.164,10.256L40.164,12.706L41.676,12.706L41.676,10.256ZM42.908,12.706L44.364000000000004,12.706L44.364000000000004,10.256L42.908,10.256L42.908,12.706ZM45.582,12.706L47.108000000000004,12.706L47.108000000000004,10.256L45.582,10.256L45.582,12.706ZM54.906,7.456L55.116,8.394L54.178,8.814L54.178,12.818Q54.178,13.434,54.031,13.735Q53.884,14.036,53.534,14.162Q53.184,14.288,52.456,14.358L51.867999999999995,14.414L51.476,13.084L52.162,13.028Q52.512,13,52.652,12.958Q52.792,12.916,52.841,12.797Q52.89,12.678,52.89,12.384L52.89,9.36Q51.980000000000004,9.724,51.322,9.948L51.013999999999996,8.576Q51.798,8.324,52.89,7.876L52.89,5.524L51.42,5.524L51.42,4.166L52.89,4.166L52.89,1.7579989999999999L54.178,1.814L54.178,4.166L55.214,4.166L55.214,5.524L54.178,5.524L54.178,7.316L54.808,7.022L54.906,7.456ZM56.894,4.5440000000000005L56.894,6.098L55.564,6.098L55.564,3.256L58.686,3.256Q58.42,2.346,58.266,1.9260000000000002L59.624,1.7579989999999999Q59.848,2.276,60.142,3.256L63.25,3.256L63.25,6.098L61.962,6.098L61.962,4.5440000000000005L56.894,4.5440000000000005ZM59.008,6.322Q58.392,6.938,57.685,7.512Q56.978,8.086,55.956,8.841999999999999L55.242,7.764Q56.824,6.728,58.126,5.37L59.008,6.322ZM60.422,5.37Q61.024,5.776,62.095,6.581Q63.166,7.386,63.656,7.806L62.942,8.982Q62.368,8.45,61.332,7.652Q60.296,6.854,59.666,6.434L60.422,5.37ZM62.592,10.256L60.044,10.256L60.044,12.566L63.572,12.566L63.572,13.826L55.144,13.826L55.144,12.566L58.63,12.566L58.63,10.256L56.054,10.256L56.054,8.982L62.592,8.982L62.592,10.256Z", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + } + ] + } + ] + } + ] + }, + "name": "AliyunIcon" +} diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx b/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx new file mode 100644 index 0000000000..5b062b8a86 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/AliyunIcon.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './AliyunIcon.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( + props, + ref, +) => <IconBase {...props} ref={ref} data={data as IconData} />) + +Icon.displayName = 'AliyunIcon' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json new file mode 100644 index 0000000000..ea60744daf --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.json @@ -0,0 +1,71 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "fill": "none", + "version": "1.1", + "width": "96", + "height": "24", + "viewBox": "0 0 96 24" + }, + "children": [ + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M6.10294,22C5.68819,22,5.18195,21.8532,4.58421,21.5595C4.46861,21.5027,4.39038,21.4658,4.34951,21.4488C3.49789,21.0943,2.74367,20.5941,2.08684,19.9484C0.695613,18.5806,0,16.9311,0,15C0,13.0689,0.695612,11.4194,2.08684,10.0516C3.24259,8.91537,4.59607,8.2511,6.14728,8.05878C6.34758,5.97414,7.22633,4.16634,8.78354,2.63539C10.5706,0.878463,12.7286,0,15.2573,0C17.2884,0,19.1146,0.595472,20.7358,1.78642C22.327,2.95528,23.4151,4.46783,24,6.32406L22.0568,6.91594C21.6024,5.47377,20.7561,4.29798,19.5181,3.38858C18.2579,2.46286,16.8377,2,15.2573,2C13.2903,2,11.6119,2.6832,10.222,4.04961C8.83217,5.41601,8.13725,7.06614,8.13725,9L8.13725,10L7.12009,10C5.71758,10,4.51932,10.4886,3.52532,11.4659C2.53132,12.4431,2.03431,13.6211,2.03431,15C2.03431,16.3789,2.53132,17.5569,3.52532,18.5341C3.99531,18.9962,4.53447,19.3538,5.14278,19.6071C5.2229,19.6405,5.33983,19.695,5.49356,19.7705C5.80505,19.9235,6.00818,20,6.10294,20L6.10294,22Z", + "fill-rule": "evenodd", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + }, + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M20.18796103515625,11.66909C19.46346103515625,11.5762,18.72726103515625,11.52975,17.991011035156248,11.52975C16.728921035156247,11.52975,15.45515103515625,11.66909,14.23981103515625,11.91292C13.02447103515625,12.156749999999999,11.85588103515625,12.539909999999999,10.73402103515625,12.98113C9.98612103515625,13.306239999999999,9.23822103515625,13.69327,8.49031803515625,14.14223C7.99950790415625,14.43251,7.85927603515625,15.10595,8.15142503515625,15.59361L11.11966103515625,19.9478C11.45855103515625,20.4354,12.13634103515625,20.5747,12.627151035156249,20.2845C12.821921035156251,20.152900000000002,13.14523103515625,19.990299999999998,13.59708103515625,19.796799999999998C14.27487103515625,19.506500000000003,14.964341035156249,19.3091,15.68887103515625,19.169800000000002C16.413401035156248,19.018900000000002,17.14962103515625,18.926000000000002,17.93258103515625,18.926000000000002L20.071061035156248,11.715530000000001L20.18796103515625,11.66909ZM22.91076103515625,12.20319L22.525161035156252,8L20.18796103515625,11.6807C20.72556103515625,11.72714,21.21636103515625,11.82003,21.74216103515625,11.92453C21.74216103515625,11.91679,22.13166103515625,12.00968,22.91076103515625,12.20319ZM18.09616103515625,18.9724L17.06782103515625,22.4557L18.773961035156248,24L21.11116103515625,23.465899999999998L21.788961035156248,19.5414C21.298161035156248,19.402,20.81896103515625,19.2511,20.32816103515625,19.1582C19.60366103515625,19.076900000000002,18.86746103515625,18.9724,18.09616103515625,18.9724ZM27.49166103515625,14.14223C26.74376103515625,13.69327,25.99586103515625,13.306239999999999,25.24796103515625,12.98113C24.52346103515625,12.69086,23.74046103515625,12.40058,22.95756103515625,12.20319L22.95756103515625,12.40058L21.69546103515625,19.5646C21.89416103515625,19.6575,22.139561035156248,19.7039,22.32646103515625,19.8084C22.77836103515625,20.0019,23.101661035156248,20.1645,23.29646103515625,20.2961C23.78726103515625,20.586399999999998,24.51176103515625,20.4354,24.80396103515625,19.959400000000002L27.77216103515625,15.605229999999999C28.16946103515625,15.05951,28.02926103515625,14.43251,27.49166103515625,14.14223Z", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + }, + { + "type": "element", + "name": "g", + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M35.785,3.8624638671875L50.233000000000004,3.8624638671875L50.233000000000004,5.9204638671875L35.785,5.9204638671875L35.785,3.8624638671875ZM43.156,11.5904638671875Q42.106,13.4594638671875,40.7515,15.4754638671875Q39.397,17.4914638671875,38.599000000000004,18.3104638671875L46.978,17.5334638671875Q45.382,15.0974638671875,44.479,13.8794638671875L46.306,12.7034638671875Q47.397999999999996,14.1104638671875,48.9835,16.3784638671875Q50.569,18.6464638671875,51.492999999999995,20.1794638671875L49.54,21.6494638671875Q49.057,20.8094638671875,48.238,19.5074638671875Q46.243,19.6334638671875,42.82,19.9274638671875Q39.397,20.2214638671875,37.024,20.4734638671875L36.184,20.5784638671875L35.47,20.6834638671875L34.84,18.5624638671875Q35.281,18.4154638671875,35.512,18.2579638671875Q35.743,18.1004638671875,35.9005,17.963963867187502Q36.058,17.8274638671875,36.121,17.7644638671875Q37.087,16.840463867187502,38.305,15.1079638671875Q39.522999999999996,13.3754638671875,40.531,11.5904638671875L34,11.5904638671875L34,9.5114638671875L52.018,9.5114638671875L52.018,11.5904638671875L43.156,11.5904638671875ZM62.203,10.9814638671875L62.203,12.7244638671875L60.25,12.7244638671875L60.25,2.5814638671875L62.203,2.6654638671875L62.203,10.4144638671875Q63.19,8.6294638671875,64.051,6.4139638671875Q64.912,4.1984638671875,65.28999999999999,2.3504638671875L67.348,2.8334628671875Q67.15899999999999,3.7784638671875,66.80199999999999,4.9754638671875L72.619,4.9754638671875L72.619,6.9704638671875L66.13,6.9704638671875Q65.143,9.7004638671875,63.778,12.0524638671875L62.203,10.9814638671875ZM56.113,3.3794638671875L58.045,3.4634638671875L58.045,12.1784638671875L56.113,12.1784638671875L56.113,3.3794638671875ZM67.495,7.3064638671875Q68.251,7.8944638671875,69.469,9.1229638671875Q70.687,10.3514638671875,71.40100000000001,11.2334638671875L69.84700000000001,12.7454638671875Q69.238,11.9684638671875,68.083,10.7714638671875Q66.928,9.5744638671875,66.025,8.7134638671875L67.495,7.3064638671875ZM70.834,13.3754638671875L70.834,18.9194638671875L73.06,18.9194638671875L73.06,20.8094638671875L54.307,20.8094638671875L54.307,18.9194638671875L56.491,18.9194638671875L56.491,13.3754638671875L70.834,13.3754638671875ZM60.733000000000004,15.2444638671875L58.465,15.2444638671875L58.465,18.9194638671875L60.733000000000004,18.9194638671875L60.733000000000004,15.2444638671875ZM62.581,18.9194638671875L64.765,18.9194638671875L64.765,15.2444638671875L62.581,15.2444638671875L62.581,18.9194638671875ZM66.592,18.9194638671875L68.881,18.9194638671875L68.881,15.2444638671875L66.592,15.2444638671875L66.592,18.9194638671875ZM80.578,11.0444638671875L80.893,12.4514638671875L79.48599999999999,13.0814638671875L79.48599999999999,19.0874638671875Q79.48599999999999,20.0114638671875,79.2655,20.4629638671875Q79.045,20.9144638671875,78.52000000000001,21.1034638671875Q77.995,21.2924638671875,76.90299999999999,21.3974638671875L76.021,21.4814638671875L75.43299999999999,19.4864638671875L76.462,19.4024638671875Q76.987,19.3604638671875,77.197,19.2974638671875Q77.407,19.2344638671875,77.4805,19.0559638671875Q77.554,18.8774638671875,77.554,18.4364638671875L77.554,13.9004638671875Q76.189,14.4464638671875,75.202,14.7824638671875L74.74000000000001,12.7244638671875Q75.916,12.3464638671875,77.554,11.6744638671875L77.554,8.1464638671875L75.34899999999999,8.1464638671875L75.34899999999999,6.1094638671875L77.554,6.1094638671875L77.554,2.4974628671875L79.48599999999999,2.5814638671875L79.48599999999999,6.1094638671875L81.03999999999999,6.1094638671875L81.03999999999999,8.1464638671875L79.48599999999999,8.1464638671875L79.48599999999999,10.8344638671875L80.431,10.3934638671875L80.578,11.0444638671875ZM83.56,6.6764638671875L83.56,9.0074638671875L81.565,9.0074638671875L81.565,4.7444638671875L86.24799999999999,4.7444638671875Q85.84899999999999,3.3794638671875,85.618,2.7494638671875L87.655,2.4974628671875Q87.991,3.2744638671875,88.432,4.7444638671875L93.094,4.7444638671875L93.094,9.0074638671875L91.162,9.0074638671875L91.162,6.6764638671875L83.56,6.6764638671875ZM86.731,9.3434638671875Q85.807,10.2674638671875,84.7465,11.1284638671875Q83.686,11.9894638671875,82.15299999999999,13.1234638671875L81.082,11.5064638671875Q83.455,9.9524638671875,85.408,7.9154638671875L86.731,9.3434638671875ZM88.852,7.9154638671875Q89.755,8.5244638671875,91.3615,9.731963867187499Q92.968,10.9394638671875,93.703,11.5694638671875L92.632,13.3334638671875Q91.771,12.5354638671875,90.217,11.3384638671875Q88.663,10.1414638671875,87.718,9.5114638671875L88.852,7.9154638671875ZM92.107,15.2444638671875L88.285,15.2444638671875L88.285,18.7094638671875L93.577,18.7094638671875L93.577,20.5994638671875L80.935,20.5994638671875L80.935,18.7094638671875L86.164,18.7094638671875L86.164,15.2444638671875L82.3,15.2444638671875L82.3,13.3334638671875L92.107,13.3334638671875L92.107,15.2444638671875Z", + "fill": "#FF6A00", + "fill-opacity": "1" + } + } + ] + } + ] + } + ] + }, + "name": "AliyunBigIcon" +} diff --git a/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx new file mode 100644 index 0000000000..0924f70fbd --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/AliyunIconBig.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './AliyunIconBig.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( + props, + ref, +) => <IconBase {...props} ref={ref} data={data as IconData} />) + +Icon.displayName = 'AliyunIconBig' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/ArizeIcon.json b/web/app/components/base/icons/src/public/tracing/ArizeIcon.json new file mode 100644 index 0000000000..fc438819ee --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/ArizeIcon.json @@ -0,0 +1,122 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "id": "Layer_2", + "data-name": "Layer 2", + "xmlns": "http://www.w3.org/2000/svg", + "viewBox": "0 0 74 16", + "width": "74", + "height": "16" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Layer_1-2", + "data-name": "Layer 1" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "style": "fill: none;", + "y": "0", + "width": "74", + "height": "16" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Arize_-_standard", + "data-name": "Arize - standard" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M67.29,9.7h-7.52c0,.08,0,.15.01.21.35,1.03,1.03,1.73,2.13,1.92,1.13.19,2.14,0,2.86-1.01.06-.09.19-.18.29-.19.66-.02,1.33,0,1.99,0-.1,1.79-2.59,3.32-5.07,3.14-2.66-.2-4.61-2.53-4.39-5.25.25-3.08,2.44-4.88,5.58-4.56,2.7.27,4.45,2.69,4.12,5.76ZM59.81,7.77h5.3c-.28-1.25-1.36-2.01-2.78-1.98-1.25.03-2.32.87-2.52,1.98Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #ff008c;", + "d": "M13.87,1.69c.4-.02.74.17.99.54,2.03,2.92,4.06,5.85,6.08,8.77.42.61.28,1.33-.29,1.73-.56.39-1.31.24-1.74-.38-1.4-2.01-2.79-4.02-4.19-6.04-.03-.04-.05-.08-.08-.11-.55-.78-1.1-.78-1.64,0-1.41,2.03-2.82,4.06-4.23,6.09-.23.34-.52.59-.93.63-.51.06-.92-.13-1.19-.57-.28-.46-.25-.92.05-1.35.68-.98,1.36-1.96,2.04-2.93,1.34-1.93,2.68-3.85,4.02-5.78.26-.37.59-.6,1.11-.59Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M35.23,13.62h-2.01v-.77c-.07.01-.1,0-.13.02-1.64,1.16-3.39,1.25-5.12.34-1.74-.92-2.47-2.51-2.45-4.45.02-2.17,1.22-3.97,3.1-4.57,1.57-.5,3.09-.45,4.46.62.02.02.06.02.14.05v-.79h2.02v9.54ZM27.57,8.88c0,.11.01.31.03.5.14,1.39,1.18,2.39,2.61,2.51,1.4.12,2.53-.63,2.92-1.97.09-.32.14-.65.15-.98.09-2.15-1.5-3.51-3.56-3.07-1.28.27-2.13,1.43-2.15,3.02Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M53.61,5.93h-4.96v-1.85h7.76c0,.54.01,1.07,0,1.61,0,.12-.12.24-.21.34-1.52,1.79-3.05,3.57-4.57,5.36-.07.09-.14.18-.26.32h5.02v1.91h-7.83c0-.54-.01-1.08,0-1.61,0-.11.11-.22.19-.31,1.55-1.83,3.1-3.65,4.64-5.47.06-.07.11-.14.21-.28Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M39.6,13.69h-2.02V4.15h2.02v1.03c.54-.32,1.04-.68,1.58-.94.57-.28,1.19-.27,1.85-.23v1.96c-.1,0-.2.02-.3.02-1.58.02-2.68.9-3.01,2.46-.08.38-.11.77-.11,1.16-.01,1.24,0,2.47,0,3.71v.38Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M44.74,4.06h1.99v9.56h-1.99V4.06Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #ff008c;", + "d": "M13.82,13.02c.84,0,1.49.64,1.5,1.46,0,.83-.68,1.53-1.5,1.52-.82,0-1.47-.67-1.48-1.5,0-.83.64-1.48,1.47-1.48Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M47.13,1.41c0,.8-.61,1.43-1.39,1.43-.8,0-1.44-.63-1.44-1.43,0-.78.63-1.41,1.42-1.41.79,0,1.41.62,1.41,1.41Z" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "ArizeIcon" +} diff --git a/web/app/components/base/icons/src/public/tracing/ArizeIcon.tsx b/web/app/components/base/icons/src/public/tracing/ArizeIcon.tsx new file mode 100644 index 0000000000..dac1ec280e --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/ArizeIcon.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './ArizeIcon.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'ArizeIcon' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/ArizeIconBig.json b/web/app/components/base/icons/src/public/tracing/ArizeIconBig.json new file mode 100644 index 0000000000..57be25e5b3 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/ArizeIconBig.json @@ -0,0 +1,122 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "id": "Layer_2", + "data-name": "Layer 2", + "xmlns": "http://www.w3.org/2000/svg", + "viewBox": "0 0 111 24", + "width": "111", + "height": "24" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Layer_1-2", + "data-name": "Layer 1" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "style": "fill: none;", + "y": "0", + "width": "111", + "height": "24" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Arize_-_standard", + "data-name": "Arize - standard" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M100.94,14.55h-11.29c0,.13-.01.23.02.31.53,1.55,1.54,2.59,3.19,2.88,1.7.29,3.22,0,4.3-1.52.09-.13.28-.27.43-.28.99-.02,1.99-.01,2.99-.01-.16,2.69-3.89,4.98-7.6,4.7-3.99-.3-6.91-3.79-6.58-7.88.37-4.62,3.67-7.31,8.37-6.85,4.05.4,6.68,4.04,6.19,8.64ZM89.71,11.66h7.96c-.43-1.88-2.04-3.02-4.17-2.97-1.87.04-3.48,1.3-3.78,2.97Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #ff008c;", + "d": "M20.81,2.53c.59-.03,1.11.26,1.49.8,3.05,4.38,6.09,8.77,9.13,13.16.63.91.43,1.99-.43,2.59-.84.59-1.97.35-2.62-.57-2.1-3.01-4.19-6.04-6.28-9.05-.04-.06-.08-.11-.12-.17-.83-1.17-1.65-1.18-2.47,0-2.11,3.05-4.23,6.09-6.34,9.14-.35.5-.77.88-1.4.95-.77.08-1.39-.19-1.79-.86-.41-.69-.38-1.38.07-2.03,1.01-1.47,2.04-2.93,3.06-4.4,2.01-2.89,4.03-5.77,6.03-8.67.39-.56.88-.9,1.67-.89Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M52.84,20.43h-3.02v-1.16c-.1.02-.16.01-.19.03-2.46,1.73-5.09,1.88-7.68.51-2.61-1.38-3.71-3.77-3.68-6.67.03-3.26,1.82-5.96,4.64-6.86,2.35-.75,4.64-.67,6.69.93.04.03.09.03.2.07v-1.18h3.03v14.31ZM41.36,13.32c.01.17.02.46.05.75.22,2.09,1.76,3.58,3.91,3.76,2.1.18,3.8-.95,4.38-2.96.14-.47.2-.98.22-1.47.13-3.22-2.25-5.27-5.33-4.61-1.92.41-3.19,2.14-3.23,4.53Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M80.41,8.9h-7.44v-2.77h11.64c0,.81.02,1.61-.01,2.41,0,.17-.19.35-.32.51-2.28,2.68-4.57,5.36-6.85,8.04-.11.13-.21.26-.38.47h7.53v2.86h-11.74c0-.82-.02-1.62.01-2.42,0-.16.17-.33.28-.47,2.32-2.74,4.64-5.47,6.96-8.21.09-.1.16-.21.32-.42Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M59.39,20.54h-3.03V6.22h3.03v1.54c.8-.48,1.55-1.01,2.37-1.41.85-.41,1.79-.41,2.77-.35v2.94c-.16.01-.3.03-.45.03-2.37.03-4.01,1.36-4.51,3.69-.12.57-.16,1.16-.16,1.74-.02,1.85,0,3.71,0,5.56v.57Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M67.1,6.09h2.99v14.33h-2.99V6.09Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #ff008c;", + "d": "M20.73,19.53c1.25,0,2.24.96,2.25,2.19.01,1.24-1.02,2.29-2.24,2.28-1.23-.01-2.21-1.01-2.22-2.24,0-1.25.96-2.22,2.21-2.22Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M70.7,2.11c0,1.19-.92,2.14-2.09,2.15-1.19.01-2.16-.95-2.16-2.14C66.46.95,67.4,0,68.58,0c1.18,0,2.12.93,2.12,2.11Z" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "ArizeIconBig" +} diff --git a/web/app/components/base/icons/src/public/tracing/ArizeIconBig.tsx b/web/app/components/base/icons/src/public/tracing/ArizeIconBig.tsx new file mode 100644 index 0000000000..f817b481e3 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/ArizeIconBig.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './ArizeIconBig.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'ArizeIconBig' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/PhoenixIcon.json b/web/app/components/base/icons/src/public/tracing/PhoenixIcon.json new file mode 100644 index 0000000000..c02847bc03 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/PhoenixIcon.json @@ -0,0 +1,853 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "id": "Layer_2", + "data-name": "Layer 2", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "viewBox": "0 0 74 16", + "width": "74", + "height": "16" + }, + "children": [ + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient", + "x1": "6.05", + "y1": ".98", + "x2": "13.18", + "y2": "13.34", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#11bab5" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".5", + "stop-color": "#00adee" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#0094c5" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-2", + "x1": "12.99", + "y1": "10.87", + "x2": "6.97", + "y2": ".45", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#fcfdff", + "stop-opacity": "0" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".12", + "stop-color": "#fcfdff", + "stop-opacity": ".17" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".3", + "stop-color": "#fcfdff", + "stop-opacity": ".42" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".47", + "stop-color": "#fcfdff", + "stop-opacity": ".63" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".64", + "stop-color": "#fcfdff", + "stop-opacity": ".79" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".78", + "stop-color": "#fcfdff", + "stop-opacity": ".9" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".91", + "stop-color": "#fcfdff", + "stop-opacity": ".97" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#fcfdff" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-3", + "x1": "11.21", + "y1": "10.8", + "x2": "6.88", + "y2": "3.29", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-4", + "x1": "10.62", + "y1": "11.35", + "x2": "8.2", + "y2": "7.16", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-5", + "x1": "10.72", + "y1": "12.2", + "x2": "9.21", + "y2": "9.59", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-6", + "x1": "8.17", + "y1": "11.96", + "x2": "8.17", + "y2": "14.71", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".03", + "stop-color": "#231f20", + "stop-opacity": ".9" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".22", + "stop-color": "#231f20", + "stop-opacity": ".4" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".42", + "stop-color": "#231f20", + "stop-opacity": ".1" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".65", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "Fade_to_Black_2", + "data-name": "Fade to Black 2", + "x1": "11.26", + "y1": "11.7", + "x2": "11.26", + "y2": "13.1", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-7", + "x1": "11.94", + "y1": "8.07", + "x2": "11.94", + "y2": "6.92", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".03", + "stop-color": "#231f20", + "stop-opacity": ".95" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".51", + "stop-color": "#231f20", + "stop-opacity": ".26" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".81", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-8", + "x1": "14.45", + "y1": "8.92", + "x2": "14.45", + "y2": "6.14", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".18", + "stop-color": "#231f20", + "stop-opacity": ".48" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".38", + "stop-color": "#231f20", + "stop-opacity": ".12" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".58", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "Fade_to_Black_2-2", + "data-name": "Fade to Black 2", + "x1": "17.03", + "y1": "11.1", + "x2": "16.07", + "y2": "9.44", + "xlink:href": "#Fade_to_Black_2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-9", + "x1": "17.8", + "y1": "10.64", + "x2": "16.36", + "y2": "8.16", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-10", + "x1": "14.07", + "y1": "10.22", + "x2": "16.16", + "y2": "9.02", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".31", + "stop-color": "#231f20", + "stop-opacity": ".5" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".67", + "stop-color": "#231f20", + "stop-opacity": ".13" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Layer_1-2", + "data-name": "Layer 1" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "style": "fill: none;", + "y": ".02", + "width": "74", + "height": "16" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Phoenix_horiz_-_gradient", + "data-name": "Phoenix horiz - gradient" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient);", + "d": "M17.91,10c-.04-.19-.14-.36-.28-.51-.06-.06-.12-.11-.19-.16-.03-.02-.07-.05-.1-.07-.04-.03-.09-.05-.14-.08-.19-.09-.34-.19-.48-.29-.06-.04-.11-.09-.16-.13-.12-.11-.21-.2-.27-.3-.02-.03-.04-.07-.05-.1,0-.01-.02-.03-.03-.04-.02-.02-.05-.02-.08-.02-.04,0-.08.04-.08.09,0,.03,0,.07,0,.11,0,.02,0,.04-.03.05,0,0,0,0-.02,0-.03,0-.06.02-.07.04-.02.02-.02.05-.02.08,0,.03.01.04.02.06h0s0,.03.01.04c.01.03.03.05.05.08.03.05.07.09.11.15.01.02.02.04,0,.06-.01.02-.03.03-.05.03-.25-.03-.46-.09-.67-.16-.02,0-.05-.02-.07-.03-.18-.08.15-.41.3-.64.66-1.02.8-2.19.81-3.38,0-1.15-.12-2.27-.35-3.39h0c-.15-.94-.27-1.29-.35-1.42-.02-.03-.04-.06-.06-.07-.02-.01-.03,0-.03,0-.04,0-.09.04-.15.1-.25.26-.23.57-.17.89.31,1.47.55,2.95.44,4.47-.07,1.04-.27,2.05-1,2.86-.04.05-.09.1-.14.14-.05.05-.14.16-.2.11-.08-.06,0-.18.02-.24.43-1,.55-2.05.48-3.12,0-.02,0-.05,0-.07h0s0-.04,0-.06c-.01-.14-.02-.28-.04-.43-.18-1.71-.4-1.82-.4-1.82h0s-.03-.02-.06-.03c-.12-.02-.18.08-.23.15-.23.35-.13.72-.05,1.08.26,1.28.27,2.55-.18,3.79-.04.11-.09.22-.14.33-.05.12-.09.26-.28.2-.18-.06-.13-.19-.1-.31.06-.31.11-.62.14-.93h0s0-.06,0-.09c0-.05,0-.11.01-.16,0-.05,0-.09,0-.13,0-.02,0-.04,0-.07.04-.99-.13-1.44-.19-1.57,0-.02-.01-.03-.02-.04h0s0,0,0,0c-.07-.1-.17-.09-.27,0-.17.17-.27.37-.22.62.13.74.05,1.46-.08,2.19-.02.1-.06.27-.27.23-.14-.03-.17-.09-.18-.21,0-.09,0-.19,0-.28,0,0,0,0,0,0h0s0-.03,0-.04h0s0-.01,0-.02c0-.13-.02-.25-.03-.38,0-.01,0-.02,0-.04-.08-.71-.18-.78-.18-.78-.07-.09-.17-.07-.26.02-.17.17-.26.37-.22.62.03.19.02.13.04.29.01.08.03.23.03.28,0,.04-.01.08-.04.11-.09.09-.23,0-.34-.05-2.08-.86-3.86-2.07-5.42-3.6-.61-.62-1.65-1.88-1.65-1.88h0s-.09-.07-.16-.04c-.14.06-.13.23-.14.38-.02.29.13.51.3.7,2.35,2.74,5.21,4.71,8.67,5.69l.62.16s.01,0,.02,0c0,0,0,0,0,0l.54.14h.01s-.04.01-.06.02c-.59.12-1.76-.1-2.69-.32-.46-.1-.92-.22-1.36-.36-.02,0-.03,0-.03,0h0c-1.73-.55-3.32-1.44-4.7-2.81-.13-.13-.25-.27-.37-.41-.11-.13-.34-.4-.41-.5,0-.01-.02-.02-.03-.03h0s0,0,0,0c-.02-.02-.05-.04-.09-.03-.1.02-.12.12-.14.21-.07.34.04.62.26.88,1.42,1.59,3.2,2.61,5.21,3.28,1.14.38,2.31.61,3.5.76-.71.23-1.44.3-2.18.28-1.43-.04-2.77-.38-4.03-.99-.46-.24-.9-.5-1.01-.58-.07-.04-.13-.07-.2-.02-.13.1-.04.27,0,.4.08.26.25.43.49.55,2.05,1.08,4.22,1.52,6.52,1.13.23-.04.45-.08.7-.16-.39.33-.83.55-1.3.72-1.44.53-2.88.51-4.31.15-.46-.12-.99-.32-.99-.32h0c-.08-.03-.15-.05-.21.04-.1.15-.01.3.08.45.21.33.63.36.95.48.11.04-.19.24-.29.31-.47.36-.94.69-1.41,1.04l-.31.23h0c-.05.04-.1.08-.08.15.03.09.12.1.2.12.34.07.62-.04.89-.24.57-.43,1.15-.84,1.71-1.28.21-.17.4-.15.7-.08-.73.55-1.4,1.06-2.08,1.57-.69.52-1.38,1.04-2.07,1.56l-.46.34s-.01,0-.02.01h0s0,0,0,0c-.04.03-.07.07-.06.11.02.1.14.13.24.15.41.07.66-.08.94-.29,1.39-1.05,2.79-2.09,4.18-3.14.31-.24.63-.37,1.08-.32-.38.29-.75.56-1.11.83l-.98.73s-.01.01-.02.02h0c-.05.04-.09.09-.07.16.04.13.19.15.33.17.24.04.45-.05.64-.19.11-.08.22-.16.33-.24h0s1.5-1.19,1.5-1.19c0,0,.35-.25.76-.49.04-.02.07-.05.11-.07.02,0,.05-.02.11-.05.04-.02.09-.05.13-.07.06-.03.14-.07.22-.12.24-.1.93-.42,1.51-1.03.27-.21.66-.48.95-.62h0s0,0,0,0c.02-.01.04-.02.07-.03h0s0,0,0,0c.01,0,.03-.01.04-.01h.04c.11-.05.28-.06.31.18,0,.06,0,.12,0,.18,0,.05-.01.1-.02.14,0,.04,0,.08.04.1,0,0,.01,0,.02,0,.04.02.09.01.12-.02.01-.01.02-.03.04-.04.15-.15.3-.26.45-.33.23-.1.47-.1.72,0,.12.05.23.11.34.19.05.03.1.07.14.12.03.03.06.05.08.08h0s.02.02.02.02c0,0,0,0,.01.01.02.02.05.02.08.02.03,0,.06-.03.07-.06,0,0,0-.01,0-.02.07-.21.09-.4.04-.59Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M13.48,7.75c-.02.1-.03.21-.05.31.02-.1.04-.21.05-.31h0Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M15.33,8.9s.02,0,.02,0c0,0-.02,0-.02,0" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "14.46 8.33 14.46 8.33 14.46 8.33" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "14.48 8.26 14.48 8.26 14.48 8.26" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "14.49 8.25 14.49 8.25 14.49 8.25" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "15.64 8.25 15.63 8.25 15.64 8.25" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M13.81,8.17s-.02.06-.04.08c.01-.03.02-.06.04-.08" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.57 8.11 12.57 8.11 12.57 8.11" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.39 8.03 12.39 8.03 12.39 8.03" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.39 8.03 12.39 8.03 12.39 8.03" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.39 8.03 12.39 8.03 12.39 8.03" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.38 8.02 12.38 8.03 12.38 8.02" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.38 8.02 12.38 8.02 12.38 8.02" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "12.35 7.89 12.35 7.89 12.35 7.89" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M12.81,7.81s0,.04-.01.06c0-.02,0-.04.01-.06" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M11.67,7.67s-.03.02-.05.03c.02,0,.03-.02.05-.03" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #e5b3a5;", + "d": "M12.39,8.03s.03.03.05.04h0s-.03-.03-.05-.04M12.39,8.03s0,0,0,0c0,0,0,0,0,0M12.39,8.03s0,0,0,0c0,0,0,0,0,0M12.38,8.03s0,0,0,0c0,0,0,0,0,0M12.38,8.02s0,0,0,0c0,0,0,0,0,0M12.38,8.02s0,0,0,0c0,0,0,0,0,0M12.35,7.89s0,0,0,0c0,0,0,0,0,0M12.35,7.89h0s0,0,0,0c0,0,0,0,0,0M11.45,7.68s0,0,0,0c.04.02.09.03.13.03.02,0,.03,0,.05,0-.02,0-.03,0-.05,0-.04,0-.09-.02-.13-.03M12.34,7.54s0,0,0,0c0,0,0,0,0,0" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #e5b3a5;", + "d": "M15.35,8.91s0,0,.01,0h0s0,0-.01,0M14.52,8.58s0,0,0,0c0,0,0,0,0,0M14.39,8.51s.01.06.04.08c.01,0,.02.01.03.01.02,0,.04,0,.05-.02-.02.01-.04.02-.05.02-.01,0-.02,0-.03-.01-.03-.02-.04-.05-.04-.08M14.46,8.33s0,0,0,.01c0,0,0,0,0-.01M14.48,8.26s-.02.05-.03.07c0-.02.02-.05.03-.07M14.49,8.25s0,0,0,0c0,0,0,0,0,0M15.63,8.25s0,0,0,0c0,0,0,0,0,0M15.64,8.24s0,0,0,0c0,0,0,0,0,0M14.49,8.24s0,0,0,0c0,0,0,0,0,0M13.4,8.21c0,.07.03.13.13.16.03.01.06.01.08.01.09,0,.13-.06.16-.13-.03.07-.07.13-.16.13-.02,0-.05,0-.08-.01-.1-.03-.13-.09-.13-.16M12.57,8.11h0,0M12.74,8.04s-.08.07-.14.07c0,0-.01,0-.02,0,0,0,.01,0,.02,0,.07,0,.11-.03.14-.07M14.05,7.54s0,0,0,0c-.03.1-.06.2-.1.3-.04.11-.09.22-.14.33h0c.05-.11.1-.22.14-.33.04-.1.07-.2.1-.3M12.87,7.49c-.02.11-.03.22-.05.33.02-.11.04-.22.06-.33h0M15.52,6.97s0,0,0,0c-.15.49-.38.95-.75,1.36-.04.05-.09.1-.14.14h0s.09-.09.14-.14c.37-.41.6-.87.75-1.36" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-2); opacity: .4;", + "d": "M4.17,2.09s-.04,0-.06.01c-.1.04-.12.14-.13.25.52.68,2.56,3.18,5.78,4.9,0,0,3.15,1.66,6.32,1.84-.25-.03-.46-.09-.67-.16-.01,0-.02,0-.04-.01h0c-1.15-.25-2.13-.56-2.83-.81.01,0,.03,0,.04,0-.01,0-.03,0-.04,0-.04,0-.07-.02-.1-.04-.55-.2-.9-.35-.98-.39-.04-.02-.08-.04-.12-.05-2.08-.86-3.86-2.07-5.42-3.6-.61-.62-1.65-1.88-1.65-1.88h0s-.06-.05-.1-.05" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-3); opacity: .4;", + "d": "M4.39,4.75s-.01,0-.02,0c-.1.02-.12.12-.14.21,0,.02,0,.03,0,.05,1.61,2.18,3.77,3.07,3.77,3.07,2.15,1.01,3.92,1.22,5,1.22.45,0,.77-.04.96-.06-.09,0-.18.01-.28.01-.63,0-1.53-.17-2.29-.35-.46-.1-.92-.22-1.36-.36-.02,0-.03,0-.03,0h0c-1.73-.55-3.32-1.44-4.7-2.81-.13-.13-.25-.27-.37-.41-.11-.13-.34-.4-.41-.5,0-.01-.02-.02-.03-.03h0s0,0,0,0c-.02-.02-.04-.03-.07-.03" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-4); opacity: .4", + "d": "M5.86,8.53s-.05,0-.08.03c-.11.08-.06.22-.02.35.81.49,2.66,1.44,4.84,1.44.83,0,1.7-.14,2.59-.48-.64.21-1.3.28-1.96.28-.07,0-.15,0-.22,0-1.43-.04-2.77-.38-4.03-.99-.46-.24-.9-.5-1.01-.58-.04-.03-.08-.05-.12-.05" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-5); opacity: .4;", + "d": "M13.34,10.6c-.35.27-.75.46-1.16.61-.77.28-1.55.41-2.32.41-.67,0-1.33-.09-1.99-.26-.46-.12-.99-.32-.99-.32h0s-.07-.03-.1-.03c-.04,0-.08.02-.11.06-.05.07-.05.14-.04.21.51.24,1.6.66,2.93.66,1.15,0,2.49-.32,3.78-1.34" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-6); opacity: .4;", + "d": "M9.91,12.25c-1.25,0-2.19-.25-2.31-.29.04.01.07.02.1.03.11.04-.19.24-.29.31-.35.27-.69.52-1.04.77h1.06c.33-.25.67-.5.99-.75.12-.1.24-.13.37-.13.1,0,.2.02.33.05-.73.55-1.4,1.06-2.08,1.57-.4.3-.79.6-1.19.9h1.07c.96-.72,1.92-1.44,2.87-2.16.22-.17.44-.28.71-.32-.2.01-.4.02-.58.02Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#Fade_to_Black_2); opacity: .4;", + "d": "M9.69,13.1h1.03l.76-.6s.35-.25.76-.49c.04-.02.07-.05.11-.07.02,0,.05-.02.11-.05.04-.02.09-.05.13-.07.07-.03.15-.08.23-.12-.73.31-1.49.46-2.18.52,0,0,.02,0,.03,0,.06,0,.12,0,.18.01-.38.29-.75.56-1.11.83l-.06.05Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-7); opacity: .4;", + "d": "M12.35,7.89c0-.09,0-.19,0-.28h0s0,0,0,0c0-.01,0-.03,0-.04h0s0-.02,0-.02c0-.08,0-.16-.02-.23-.25-.1-.48-.24-.69-.39,0,.02,0,.04,0,.07.03.19.02.13.04.29.01.08.03.23.03.28,0,.04-.01.08-.04.11-.03.03-.06.04-.09.04-.04,0-.09-.02-.13-.03.08.04.43.19.98.39-.06-.04-.08-.09-.08-.18Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-8); opacity: .4;", + "d": "M16.36,6.14c-.23.32-.51.6-.84.83-.15.49-.38.95-.75,1.36-.04.05-.09.1-.14.14-.05.04-.11.12-.17.12-.01,0-.02,0-.03-.01-.08-.06,0-.18.02-.24.14-.32.24-.65.32-.98-.23.09-.47.15-.72.18-.03.1-.06.2-.1.3-.04.11-.09.22-.14.33-.05.1-.08.21-.2.21-.02,0-.05,0-.08-.01-.18-.06-.13-.19-.1-.31.03-.16.06-.32.08-.49-.22,0-.43-.04-.64-.08-.02.13-.04.26-.07.39-.02.09-.05.24-.21.24-.02,0-.04,0-.06,0,.69.25,1.68.56,2.83.81h0s-.02,0-.03-.01c-.18-.08.15-.41.3-.64.43-.66.64-1.37.73-2.12Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#Fade_to_Black_2-2); opacity: .5;", + "d": "M16.19,9.7c-.14,0-.29.01-.43.04-.03,0-.05,0-.08,0-.04,0-.08,0-.11-.02,0,0,.01.01.02.02.06.09.1.18.11.28.02.02.03.04.04.07h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0t0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,.05,0,.07c0,.04,0,.07,0,.11,0,.05-.01.1-.02.14,0,0,0,.01,0,.02,0,.03.02.06.04.08h.02s.03.02.05.02c.03,0,.06-.01.08-.03.01-.01.02-.03.04-.04.15-.15.3-.26.45-.33.12-.05.23-.07.35-.07s.24.02.36.07c.12.05.23.11.34.19.05.03.1.07.14.12.03.03.06.05.08.08h0s.02.02.02.02h.01s.04.03.06.03c-.1-.21-.22-.41-.4-.56-.33-.29-.75-.41-1.18-.41" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-9); opacity: .4;", + "d": "M16.15,8.3h-.02s-.08.05-.08.09c0,.03,0,.07,0,.11,0,.01,0,.02,0,.03.18.26.56.73,1.15.91,0,0,.91.47.68,1.16h0v-.02c.08-.21.09-.4.05-.59-.04-.19-.14-.36-.28-.51-.06-.06-.12-.11-.19-.16-.03-.02-.07-.05-.1-.07-.04-.03-.09-.05-.14-.08-.19-.09-.34-.19-.48-.29-.06-.04-.11-.09-.16-.13-.12-.11-.21-.2-.27-.3-.02-.03-.04-.07-.05-.1,0-.01-.02-.03-.03-.04-.02-.01-.04-.02-.06-.02" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-10); opacity: .5;", + "d": "M15,10.21l.18-.11.77-1.03s-.42-.08-.63-.17c0,0-.21.96-.99,1.77l.29-.21c.11-.08.22-.15.38-.25Z" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M25.33,4.73h1.97c1.42,0,2.07.73,2.07,2.23v.74c0,1.5-.65,2.23-2.07,2.23h-1.14v4.09h-.82V4.73ZM27.3,9.19c.86,0,1.25-.39,1.25-1.44v-.84c0-1.04-.39-1.44-1.25-1.44h-1.14v3.71h1.14Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M32.4,4.73h.82v4.08h2.54v-4.08h.82v9.3h-.82v-4.48h-2.54v4.48h-.82V4.73Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M39.82,11.9v-5.02c0-1.48.74-2.27,2.09-2.27s2.09.8,2.09,2.27v5.02c0,1.48-.74,2.27-2.09,2.27s-2.09-.8-2.09-2.27ZM43.17,11.95v-5.13c0-1-.45-1.48-1.26-1.48s-1.26.48-1.26,1.48v5.13c0,1,.45,1.46,1.26,1.46s1.26-.47,1.26-1.46Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M47.23,4.73h3.72v.74h-2.9v3.34h2.38v.74h-2.38v3.72h2.9v.76h-3.72V4.73Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M53.93,4.73h1.09l2.42,7.19v-7.19h.77v9.3h-.88l-2.64-7.93v7.93h-.76V4.73Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M61.55,4.73h.82v9.3h-.82V4.73Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M67.19,9.29l-1.82-4.56h.88l1.46,3.69,1.48-3.69h.8l-1.82,4.56,1.9,4.74h-.88l-1.54-3.91-1.55,3.91h-.8l1.9-4.74Z" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "PhoenixIcon" +} diff --git a/web/app/components/base/icons/src/public/tracing/PhoenixIcon.tsx b/web/app/components/base/icons/src/public/tracing/PhoenixIcon.tsx new file mode 100644 index 0000000000..e0d36e065d --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/PhoenixIcon.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './PhoenixIcon.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'PhoenixIcon' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.json b/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.json new file mode 100644 index 0000000000..f48a2f53ae --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.json @@ -0,0 +1,853 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "id": "Layer_2", + "data-name": "Layer 2", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "viewBox": "0 0 111 24", + "width": "111", + "height": "24" + }, + "children": [ + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient", + "x1": "9.07", + "y1": "1.47", + "x2": "19.77", + "y2": "20", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#11bab5" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".5", + "stop-color": "#00adee" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#0094c5" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-2", + "x1": "19.48", + "y1": "16.3", + "x2": "10.46", + "y2": ".67", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#fcfdff", + "stop-opacity": "0" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".12", + "stop-color": "#fcfdff", + "stop-opacity": ".17" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".3", + "stop-color": "#fcfdff", + "stop-opacity": ".42" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".47", + "stop-color": "#fcfdff", + "stop-opacity": ".63" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".64", + "stop-color": "#fcfdff", + "stop-opacity": ".79" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".78", + "stop-color": "#fcfdff", + "stop-opacity": ".9" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".91", + "stop-color": "#fcfdff", + "stop-opacity": ".97" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#fcfdff" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-3", + "x1": "16.82", + "y1": "16.21", + "x2": "10.32", + "y2": "4.94", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-4", + "x1": "15.92", + "y1": "17.03", + "x2": "12.29", + "y2": "10.74", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-5", + "x1": "16.08", + "y1": "18.3", + "x2": "13.82", + "y2": "14.38", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-6", + "x1": "12.26", + "y1": "17.94", + "x2": "12.26", + "y2": "22.07", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".03", + "stop-color": "#231f20", + "stop-opacity": ".9" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".22", + "stop-color": "#231f20", + "stop-opacity": ".4" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".42", + "stop-color": "#231f20", + "stop-opacity": ".1" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".65", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "Fade_to_Black_2", + "data-name": "Fade to Black 2", + "x1": "16.89", + "y1": "17.55", + "x2": "16.89", + "y2": "19.66", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-7", + "x1": "17.91", + "y1": "12.1", + "x2": "17.91", + "y2": "10.38", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".03", + "stop-color": "#231f20", + "stop-opacity": ".95" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".51", + "stop-color": "#231f20", + "stop-opacity": ".26" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".81", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-8", + "x1": "21.67", + "y1": "13.37", + "x2": "21.67", + "y2": "9.21", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".18", + "stop-color": "#231f20", + "stop-opacity": ".48" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".38", + "stop-color": "#231f20", + "stop-opacity": ".12" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".58", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "Fade_to_Black_2-2", + "data-name": "Fade to Black 2", + "x1": "25.54", + "y1": "16.65", + "x2": "24.1", + "y2": "14.16", + "xlink:href": "#Fade_to_Black_2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-9", + "x1": "26.7", + "y1": "15.97", + "x2": "24.55", + "y2": "12.24", + "xlink:href": "#linear-gradient-2" + }, + "children": [] + }, + { + "type": "element", + "name": "linearGradient", + "attributes": { + "id": "linear-gradient-10", + "x1": "21.11", + "y1": "15.34", + "x2": "24.24", + "y2": "13.53", + "gradientUnits": "userSpaceOnUse" + }, + "children": [ + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "0", + "stop-color": "#231f20" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".31", + "stop-color": "#231f20", + "stop-opacity": ".5" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": ".67", + "stop-color": "#231f20", + "stop-opacity": ".13" + }, + "children": [] + }, + { + "type": "element", + "name": "stop", + "attributes": { + "offset": "1", + "stop-color": "#231f20", + "stop-opacity": "0" + }, + "children": [] + } + ] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Layer_1-2", + "data-name": "Layer 1" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "style": "fill: none;", + "y": ".04", + "width": "111", + "height": "24" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "id": "Phoenix_horiz_-_gradient", + "data-name": "Phoenix horiz - gradient" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient);", + "d": "M26.87,15c-.06-.28-.2-.53-.42-.76-.08-.09-.18-.17-.28-.25-.05-.04-.1-.07-.15-.1-.07-.04-.14-.08-.21-.12-.28-.14-.51-.29-.72-.44-.08-.06-.17-.13-.24-.19-.19-.16-.31-.3-.4-.45-.03-.05-.06-.1-.08-.15-.01-.02-.03-.04-.05-.05-.03-.02-.08-.04-.12-.03-.07.01-.12.07-.13.13,0,.05,0,.11,0,.17,0,.03-.01.07-.04.08,0,0-.01,0-.02,0-.04,0-.08.03-.11.07-.03.04-.03.08-.02.13,0,.04.02.06.03.09v.02s.02.03.03.04c.02.04.04.08.07.12.05.07.1.14.17.22.02.02.02.06,0,.09-.02.03-.05.04-.08.04-.37-.05-.7-.13-1-.25-.04-.01-.07-.03-.11-.04-.28-.12.22-.61.45-.96,1-1.53,1.2-3.29,1.21-5.07,0-1.72-.18-3.41-.52-5.08h0c-.22-1.4-.41-1.93-.53-2.13-.03-.05-.05-.08-.08-.1-.03-.02-.05-.01-.05-.01-.06,0-.14.06-.23.15-.37.39-.35.86-.25,1.34.47,2.21.82,4.43.66,6.7-.11,1.56-.4,3.07-1.5,4.3-.07.07-.14.14-.21.21-.08.08-.21.24-.3.17-.13-.09-.01-.27.03-.36.64-1.5.82-3.07.73-4.69,0-.04,0-.07,0-.11h0s0-.06,0-.09c-.02-.21-.04-.43-.06-.64-.28-2.56-.61-2.73-.61-2.73h0s-.05-.03-.09-.04c-.17-.04-.27.12-.35.23-.35.52-.19,1.07-.08,1.62.39,1.92.4,3.82-.28,5.69-.06.17-.14.34-.21.5-.08.17-.14.39-.42.3-.26-.09-.19-.29-.16-.47.09-.47.16-.93.2-1.4h0s0-.09.01-.13c0-.08.01-.16.02-.24,0-.07,0-.13.01-.2,0-.03,0-.07,0-.1.05-1.48-.2-2.16-.29-2.36-.01-.02-.02-.05-.03-.07h0s0,0,0,0c-.1-.15-.26-.13-.4,0-.26.25-.4.56-.33.93.19,1.1.08,2.2-.13,3.28-.03.15-.09.41-.4.34-.21-.05-.26-.14-.27-.32,0-.14,0-.28,0-.42,0,0,0,0,0-.01h0s0-.04,0-.06h0s0-.02,0-.03c0-.19-.02-.38-.05-.57,0-.02,0-.04,0-.05-.12-1.07-.27-1.17-.27-1.17-.1-.13-.25-.11-.39.03-.26.25-.39.55-.33.93.04.28.03.19.06.43.02.12.05.35.05.42,0,.06-.02.12-.06.16-.13.14-.34,0-.5-.07-3.12-1.29-5.79-3.1-8.12-5.4-.92-.94-2.48-2.83-2.48-2.83h0c-.06-.07-.13-.11-.24-.06-.2.09-.2.35-.22.57-.04.44.2.76.45,1.06,3.52,4.11,7.82,7.06,13,8.54l.93.25s.02,0,.03,0c0,0,0,0,0,0l.8.21h.02s-.06.02-.09.03c-.88.17-2.63-.15-4.04-.47-.7-.15-1.38-.32-2.05-.53-.03,0-.05-.01-.05-.01h0c-2.6-.83-4.97-2.17-7.05-4.21-.2-.19-.38-.4-.56-.61-.16-.2-.5-.61-.62-.75-.01-.02-.03-.03-.04-.05h0s0,0,0,0c-.04-.04-.08-.06-.14-.05-.14.03-.19.18-.21.31-.11.51.05.93.39,1.31,2.13,2.39,4.8,3.91,7.81,4.91,1.71.57,3.46.91,5.25,1.13-1.07.35-2.16.45-3.27.42-2.14-.05-4.15-.57-6.04-1.49-.7-.36-1.34-.76-1.52-.86-.1-.07-.2-.11-.3-.03-.19.15-.06.4,0,.6.12.38.38.65.73.83,3.08,1.63,6.32,2.28,9.78,1.7.35-.06.68-.12,1.05-.25-.58.5-1.25.83-1.95,1.08-2.17.79-4.32.76-6.46.22-.69-.18-1.49-.48-1.49-.48h0c-.12-.05-.23-.07-.32.06-.16.22-.02.46.12.67.32.5.94.55,1.43.72.17.05-.29.36-.43.47-.71.54-1.4,1.04-2.11,1.56l-.46.34h0c-.08.05-.15.12-.11.23.04.13.18.15.31.18.51.1.94-.06,1.33-.36.86-.64,1.73-1.26,2.56-1.93.32-.25.61-.23,1.04-.12-1.09.82-2.11,1.59-3.13,2.36-1.03.78-2.07,1.56-3.1,2.33l-.68.51s-.02.01-.03.02h-.01s0,0,0,0c-.06.05-.1.1-.09.17.03.15.21.19.36.22.61.11.99-.12,1.41-.44,2.09-1.57,4.19-3.13,6.27-4.71.47-.36.95-.56,1.62-.48-.57.43-1.12.84-1.67,1.25l-1.47,1.09s-.02.02-.03.02h0c-.08.06-.13.13-.1.24.06.19.29.22.49.25.37.05.68-.08.96-.29.17-.12.33-.24.5-.37h0s2.25-1.79,2.25-1.79c0,0,.53-.38,1.15-.73.05-.03.11-.07.17-.1.02-.01.08-.04.17-.08.07-.03.13-.07.2-.1.1-.05.21-.11.33-.18.36-.15,1.4-.64,2.26-1.55.41-.32.98-.73,1.43-.92h0s0,0,0,0c.03-.02.07-.03.1-.04h0s0,0,0,0c.02,0,.04-.02.06-.02l.06-.02c.16-.05.43-.06.46.29,0,.09,0,.18,0,.27,0,.07-.02.15-.03.21-.01.06.01.12.06.15,0,0,.02.01.02.01.06.03.14.02.18-.03.02-.02.03-.04.05-.06.22-.23.44-.39.68-.49.35-.14.7-.14,1.07,0,.18.07.35.16.51.28.07.05.14.11.21.17.04.04.08.08.12.12h0s.03.04.03.04c0,0,.01.01.02.02.04.03.08.04.12.03.05-.01.09-.05.11-.1,0,0,0-.02.01-.03.11-.31.13-.6.07-.89Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M20.22,11.62c-.03.16-.05.31-.08.47.03-.16.06-.31.08-.47h0Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M22.99,13.35s.02,0,.03.01c-.01,0-.02,0-.03-.01" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "21.68 12.5 21.68 12.5 21.68 12.5" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "21.73 12.39 21.73 12.39 21.73 12.39" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "21.74 12.37 21.73 12.38 21.74 12.37" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "23.45 12.37 23.45 12.38 23.45 12.37" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M20.72,12.26s-.04.08-.06.12c.02-.04.04-.08.06-.12" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.86 12.17 18.86 12.17 18.86 12.17" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.58 12.04 18.58 12.04 18.58 12.04" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.58 12.04 18.58 12.04 18.58 12.04" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.58 12.04 18.58 12.04 18.58 12.04" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.58 12.04 18.58 12.04 18.58 12.04" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.58 12.03 18.58 12.04 18.58 12.03" + }, + "children": [] + }, + { + "type": "element", + "name": "polyline", + "attributes": { + "style": "fill: #fddab4;", + "points": "18.52 11.84 18.52 11.84 18.52 11.84" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M19.22,11.72s-.01.06-.02.09c0-.03.01-.06.02-.09" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #fddab4;", + "d": "M17.51,11.51s-.04.04-.07.05c.02,0,.05-.02.07-.05" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #e5b3a5;", + "d": "M18.58,12.04s.04.04.07.06h0s-.05-.04-.07-.06M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.58,12.04s0,0,0,0c0,0,0,0,0,0M18.57,12.03s0,0,0,0c0,0,0,0,0,0M18.52,11.84s0,0,0,0c0,0,0,0,0,0M18.52,11.84h0s0,0,0,0c0,0,0,0,0,0M17.17,11.51s0,0,0,0c.06.03.13.05.19.05.03,0,.05,0,.07-.01-.02,0-.05.01-.07.01-.06,0-.13-.02-.19-.05M18.52,11.31s0,0,0,0c0,0,0,0,0,0" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #e5b3a5;", + "d": "M23.02,13.37s.01,0,.02,0h0s-.01,0-.02,0M21.78,12.86s0,0,0,0c0,0,0,0,0,0M21.59,12.76s.02.08.06.11c.02.01.03.02.05.02.03,0,.05-.01.08-.03-.03.02-.05.03-.08.03-.02,0-.03,0-.05-.02-.04-.03-.06-.07-.06-.11M21.68,12.5s0,.01,0,.02c0,0,0-.01,0-.02M21.73,12.39s-.03.07-.04.11c.01-.03.03-.07.04-.11M21.73,12.38s0,0,0,.01c0,0,0,0,0-.01M23.45,12.38s0,0,0,0c0,0,0,0,0,0M23.45,12.37s0,0,0,0c0,0,0,0,0,0M21.74,12.37s0,0,0,0c0,0,0,0,0,0M20.1,12.32c0,.1.04.19.19.24.05.02.09.02.12.02.13,0,.19-.09.24-.2-.05.1-.11.2-.24.2-.04,0-.08,0-.12-.02-.15-.05-.19-.14-.19-.24M18.86,12.17h0,0M19.11,12.07c-.05.06-.11.1-.22.1-.01,0-.02,0-.03,0,.01,0,.02,0,.03,0,.1,0,.17-.04.22-.1M21.08,11.32s0,0,0,0c-.05.15-.09.3-.15.44-.06.17-.14.34-.21.5h0c.08-.16.15-.33.21-.5.05-.15.1-.3.15-.45M19.3,11.23c-.02.16-.05.33-.08.49.03-.16.06-.33.08-.49h0M23.28,10.45s0,0,0,0c-.22.73-.57,1.42-1.12,2.04-.07.07-.14.14-.21.21h0c.07-.07.14-.14.21-.21.55-.62.9-1.31,1.12-2.04" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-2); opacity: .4;", + "d": "M6.25,3.13s-.06,0-.09.02c-.14.06-.18.21-.2.37.78,1.02,3.84,4.77,8.67,7.35,0,0,4.72,2.49,9.48,2.76-.37-.05-.7-.13-1-.25-.02,0-.04-.01-.05-.02h0c-1.73-.37-3.2-.84-4.24-1.21.02,0,.04,0,.06,0-.02,0-.04,0-.06,0-.06-.01-.11-.03-.15-.05-.82-.3-1.35-.53-1.47-.59-.06-.03-.12-.06-.17-.08-3.12-1.29-5.79-3.1-8.12-5.4-.92-.94-2.48-2.83-2.48-2.83h0s-.09-.08-.15-.08" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-3); opacity: .4;", + "d": "M6.59,7.12s-.02,0-.03,0c-.14.03-.19.18-.21.31,0,.02,0,.05-.01.07,2.41,3.27,5.65,4.6,5.65,4.6,3.23,1.52,5.88,1.83,7.5,1.83.67,0,1.16-.05,1.44-.09-.13.01-.27.02-.42.02-.95,0-2.3-.26-3.43-.52-.7-.15-1.38-.32-2.05-.53-.03,0-.05-.01-.05-.01h0c-2.6-.83-4.97-2.17-7.05-4.21-.2-.19-.38-.4-.56-.61-.16-.2-.5-.61-.62-.75-.01-.02-.03-.03-.04-.05h0s0,0,0,0c-.03-.03-.06-.05-.1-.05" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-4); opacity: .4;", + "d": "M8.78,12.8s-.08.01-.12.04c-.16.13-.09.34-.02.52,1.21.73,3.98,2.16,7.27,2.16,1.24,0,2.55-.2,3.88-.72-.96.31-1.94.43-2.94.43-.11,0-.22,0-.33,0-2.14-.05-4.15-.57-6.04-1.49-.7-.36-1.34-.76-1.52-.86-.06-.04-.12-.07-.18-.07" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-5); opacity: .4;", + "d": "M20.02,15.9c-.53.4-1.12.68-1.74.91-1.16.42-2.32.61-3.47.61-1,0-1.99-.14-2.99-.39-.69-.18-1.49-.48-1.49-.48h0c-.05-.02-.1-.04-.15-.04-.06,0-.12.03-.17.1-.07.1-.08.21-.06.32.76.35,2.4.98,4.39.98,1.73,0,3.73-.47,5.67-2.01" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-6); opacity: .4;", + "d": "M14.87,18.37c-1.87,0-3.29-.38-3.47-.43.05.02.1.03.15.05.17.05-.29.36-.43.47-.52.4-1.04.78-1.56,1.16h1.59c.5-.37,1-.74,1.49-1.13.18-.14.35-.2.55-.2.15,0,.31.03.49.08-1.09.82-2.11,1.59-3.13,2.36-.6.45-1.19.9-1.79,1.34h1.6c1.44-1.08,2.88-2.15,4.31-3.24.33-.25.67-.42,1.07-.48-.3.02-.59.03-.88.03Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#Fade_to_Black_2); opacity: .4;", + "d": "M14.54,19.66h1.55l1.14-.91s.53-.38,1.15-.73c.05-.03.11-.07.17-.1.02-.01.08-.04.17-.08.07-.03.13-.07.2-.1.1-.05.22-.11.35-.19-1.1.46-2.23.69-3.28.77.01,0,.03,0,.04,0,.09,0,.18,0,.27.02-.57.43-1.12.84-1.67,1.25l-.09.07Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-7); opacity: .4;", + "d": "M18.52,11.84c0-.14,0-.28,0-.42h0s0-.01,0-.01c0-.02,0-.04,0-.06h0s0-.03,0-.03c0-.12-.01-.23-.03-.35-.37-.16-.72-.35-1.04-.59,0,.03,0,.07,0,.1.04.28.03.19.06.43.02.12.05.35.05.42,0,.06-.02.12-.06.16-.04.04-.09.06-.14.06-.06,0-.13-.02-.19-.05.12.06.65.29,1.48.59-.09-.05-.12-.14-.12-.27Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-8); opacity: .4;", + "d": "M24.54,9.21c-.34.48-.77.9-1.26,1.24-.22.73-.57,1.42-1.12,2.04-.07.07-.14.14-.21.21-.07.07-.17.19-.25.19-.02,0-.03,0-.05-.02-.13-.09-.01-.27.03-.36.21-.48.37-.98.48-1.47-.35.13-.71.22-1.08.27-.05.15-.09.3-.15.44-.06.17-.14.34-.21.5-.07.14-.12.32-.3.32-.04,0-.08,0-.12-.02-.26-.09-.19-.29-.16-.47.05-.24.09-.49.12-.73-.33-.01-.65-.05-.96-.12-.03.19-.06.39-.1.58-.02.13-.08.35-.31.35-.03,0-.06,0-.09-.01,1.04.37,2.51.84,4.24,1.21h0s-.03-.01-.05-.02c-.28-.12.22-.61.45-.96.64-.98.95-2.06,1.1-3.18Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#Fade_to_Black_2-2); opacity: .5;", + "d": "M24.28,14.56c-.22,0-.43.02-.65.06-.04,0-.08.01-.12.01-.06,0-.12,0-.17-.03,0,.01.02.02.03.03.09.13.14.27.16.42.02.03.04.06.06.1h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0t0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0c0,0,0,0,0,0,0,0,0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,0,0,0h0s0,.07,0,.1c0,.05,0,.11,0,.17,0,.07-.02.15-.03.21,0,0,0,.02,0,.03,0,.05.02.09.06.12h.02s.05.03.07.03c.04,0,.08-.02.11-.05.02-.02.03-.04.05-.06.22-.23.44-.39.68-.49.17-.07.35-.11.53-.11s.36.04.55.11c.18.07.35.16.51.28.07.05.14.11.21.17.04.04.08.08.12.12h0s.03.04.03.04l.02.02s.06.03.09.03c-.15-.32-.33-.61-.6-.84-.5-.43-1.13-.62-1.77-.62" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-9); opacity: .4;", + "d": "M24.22,12.45h-.03c-.07.01-.12.07-.13.14,0,.05,0,.11,0,.17,0,.02,0,.04-.01.05.27.4.84,1.1,1.72,1.36,0,0,1.36.71,1.01,1.74h0v-.03c.12-.31.14-.6.08-.89-.06-.28-.2-.53-.42-.76-.08-.09-.18-.17-.28-.25-.05-.04-.1-.07-.15-.1-.07-.04-.14-.08-.21-.12-.28-.14-.51-.29-.72-.44-.08-.06-.17-.13-.24-.19-.19-.16-.31-.3-.4-.45-.03-.05-.06-.1-.08-.15-.01-.02-.03-.04-.05-.05-.03-.02-.06-.03-.09-.03" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: url(#linear-gradient-10); opacity: .5;", + "d": "M22.5,15.32l.28-.16,1.16-1.55s-.63-.12-.94-.26c0,0-.32,1.45-1.49,2.66l.43-.32c.17-.12.33-.23.56-.37Z" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M38,7.1h2.95c2.13,0,3.11,1.1,3.11,3.35v1.12c0,2.25-.98,3.35-3.11,3.35h-1.71v6.14h-1.24V7.1ZM40.95,13.78c1.3,0,1.87-.58,1.87-2.15v-1.26c0-1.55-.58-2.15-1.87-2.15h-1.71v5.56h1.71Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M48.61,7.1h1.24v6.12h3.81v-6.12h1.24v13.95h-1.24v-6.72h-3.81v6.72h-1.24V7.1Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M59.73,17.84v-7.54c0-2.21,1.12-3.41,3.13-3.41s3.13,1.2,3.13,3.41v7.54c0,2.21-1.12,3.41-3.13,3.41s-3.13-1.2-3.13-3.41ZM64.75,17.92v-7.69c0-1.5-.68-2.21-1.89-2.21s-1.89.72-1.89,2.21v7.69c0,1.5.68,2.19,1.89,2.19s1.89-.7,1.89-2.19Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M70.85,7.1h5.58v1.12h-4.35v5h3.57v1.12h-3.57v5.58h4.35v1.14h-5.58V7.1Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M80.9,7.1h1.63l3.63,10.78V7.1h1.16v13.95h-1.32l-3.97-11.9v11.9h-1.14V7.1Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M92.32,7.1h1.24v13.95h-1.24V7.1Z" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "style": "fill: #404041;", + "d": "M100.79,13.94l-2.73-6.84h1.32l2.19,5.54,2.21-5.54h1.2l-2.73,6.84,2.85,7.12h-1.32l-2.31-5.86-2.33,5.86h-1.2l2.85-7.12Z" + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + "name": "PhoenixIconBig" +} diff --git a/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.tsx b/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.tsx new file mode 100644 index 0000000000..9131e6bea6 --- /dev/null +++ b/web/app/components/base/icons/src/public/tracing/PhoenixIconBig.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './PhoenixIconBig.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'PhoenixIconBig' + +export default Icon diff --git a/web/app/components/base/icons/src/public/tracing/index.ts b/web/app/components/base/icons/src/public/tracing/index.ts index 36b59e479b..07e3385f46 100644 --- a/web/app/components/base/icons/src/public/tracing/index.ts +++ b/web/app/components/base/icons/src/public/tracing/index.ts @@ -1,9 +1,15 @@ +export { default as ArizeIconBig } from './ArizeIconBig' +export { default as ArizeIcon } from './ArizeIcon' export { default as LangfuseIconBig } from './LangfuseIconBig' export { default as LangfuseIcon } from './LangfuseIcon' export { default as LangsmithIconBig } from './LangsmithIconBig' export { default as LangsmithIcon } from './LangsmithIcon' export { default as OpikIconBig } from './OpikIconBig' export { default as OpikIcon } from './OpikIcon' +export { default as PhoenixIconBig } from './PhoenixIconBig' +export { default as PhoenixIcon } from './PhoenixIcon' export { default as TracingIcon } from './TracingIcon' export { default as WeaveIconBig } from './WeaveIconBig' export { default as WeaveIcon } from './WeaveIcon' +export { default as AliyunIconBig } from './AliyunIconBig' +export { default as AliyunIcon } from './AliyunIcon' diff --git a/web/app/components/base/icons/src/vender/line/others/SearchMenu.json b/web/app/components/base/icons/src/vender/line/others/SearchMenu.json new file mode 100644 index 0000000000..5222574040 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/others/SearchMenu.json @@ -0,0 +1,77 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "32", + "height": "32", + "viewBox": "0 0 32 32", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M28.0049 16C28.0049 20.4183 24.4231 24 20.0049 24C15.5866 24 12.0049 20.4183 12.0049 16C12.0049 11.5817 15.5866 8 20.0049 8C24.4231 8 28.0049 11.5817 28.0049 16Z", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 16H6.67155", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 9.33334H8.00488", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.00488 22.6667H8.00488", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M26 22L29.3333 25.3333", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + "name": "SearchMenu" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/others/SearchMenu.tsx b/web/app/components/base/icons/src/vender/line/others/SearchMenu.tsx new file mode 100644 index 0000000000..4826abb20f --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/others/SearchMenu.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './SearchMenu.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'SearchMenu' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/others/index.ts b/web/app/components/base/icons/src/vender/line/others/index.ts index 19d5f1ebb5..2322e9d9f1 100644 --- a/web/app/components/base/icons/src/vender/line/others/index.ts +++ b/web/app/components/base/icons/src/vender/line/others/index.ts @@ -9,4 +9,5 @@ export { default as GlobalVariable } from './GlobalVariable' export { default as Icon3Dots } from './Icon3Dots' export { default as LongArrowLeft } from './LongArrowLeft' export { default as LongArrowRight } from './LongArrowRight' +export { default as SearchMenu } from './SearchMenu' export { default as Tools } from './Tools' diff --git a/web/app/components/base/icons/src/vender/other/Mcp.json b/web/app/components/base/icons/src/vender/other/Mcp.json new file mode 100644 index 0000000000..7caa70b16b --- /dev/null +++ b/web/app/components/base/icons/src/vender/other/Mcp.json @@ -0,0 +1,35 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M9.20626 1.68651C9.61828 1.68651 10.014 1.8473 10.3093 2.13466C10.4536 2.27516 10.5684 2.44313 10.6468 2.62868C10.7252 2.81422 10.7657 3.01358 10.7659 3.21501C10.7661 3.41645 10.7259 3.61588 10.6478 3.80156C10.5697 3.98723 10.4552 4.1554 10.3111 4.29614L5.86656 8.65516C5.81837 8.70203 5.78006 8.75808 5.7539 8.82001C5.72775 8.88194 5.71427 8.94848 5.71427 9.01571C5.71427 9.08294 5.72775 9.14948 5.7539 9.21141C5.78006 9.27334 5.81837 9.32939 5.86656 9.37626C5.96503 9.47212 6.09703 9.52576 6.23445 9.52576C6.37187 9.52576 6.50387 9.47212 6.60234 9.37626L6.66222 9.31698L6.66345 9.31576L11.0463 5.01725C11.3417 4.73067 11.7372 4.57056 12.1488 4.5709C12.5604 4.57124 12.9556 4.73202 13.2506 5.01908L13.2811 5.04902C13.4256 5.18967 13.5405 5.35786 13.6189 5.54363C13.6973 5.72941 13.7377 5.92903 13.7377 6.13068C13.7377 6.33233 13.6973 6.53195 13.6189 6.71773C13.5405 6.9035 13.4256 7.07169 13.2811 7.21234L7.96082 12.43C7.84828 12.5393 7.75882 12.6701 7.69773 12.8147C7.63664 12.9592 7.60517 13.1145 7.60517 13.2714C7.60517 13.4284 7.63664 13.5837 7.69773 13.7282C7.75882 13.8728 7.84828 14.0036 7.96082 14.1129L9.05348 15.1842C9.15192 15.2799 9.28378 15.3334 9.42106 15.3334C9.55834 15.3334 9.6902 15.2799 9.78864 15.1842C9.83683 15.1373 9.87514 15.0813 9.9013 15.0194C9.92746 14.9574 9.94094 14.8909 9.94094 14.8237C9.94094 14.7564 9.92746 14.6899 9.9013 14.628C9.87514 14.566 9.83683 14.51 9.78864 14.4631L8.69598 13.3912C8.67992 13.3756 8.66716 13.357 8.65844 13.3363C8.64973 13.3157 8.64523 13.2935 8.64523 13.2711C8.64523 13.2488 8.64973 13.2266 8.65844 13.206C8.66716 13.1853 8.67992 13.1667 8.69598 13.1511L14.0163 7.93405C14.2572 7.69971 14.4488 7.41943 14.5796 7.10979C14.7104 6.80014 14.7778 6.46742 14.7778 6.13129C14.7778 5.79516 14.7104 5.46244 14.5796 5.1528C14.4488 4.84315 14.2572 4.56288 14.0163 4.32853L13.9857 4.29797C13.6978 4.01697 13.3493 3.80582 12.9669 3.6808C12.5845 3.55578 12.1785 3.52022 11.7802 3.57687C11.8371 3.1838 11.8001 2.78285 11.6722 2.40684C11.5443 2.03083 11.3292 1.69045 11.0445 1.41356C10.5524 0.93469 9.89288 0.666748 9.20626 0.666748C8.51964 0.666748 7.86012 0.93469 7.36805 1.41356L1.48555 7.18239C1.43735 7.22926 1.39905 7.28532 1.37289 7.34725C1.34673 7.40917 1.33325 7.47572 1.33325 7.54294C1.33325 7.61017 1.34673 7.67672 1.37289 7.73864C1.39905 7.80057 1.43735 7.85663 1.48555 7.9035C1.58399 7.99918 1.71585 8.0527 1.85313 8.0527C1.9904 8.0527 2.12227 7.99918 2.22071 7.9035L8.10321 2.13466C8.39848 1.8473 8.79424 1.68651 9.20626 1.68651Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M9.68688 3.41201C9.66072 3.47394 9.62241 3.52999 9.57422 3.57686L5.22314 7.8436C5.07864 7.98425 4.96378 8.15243 4.88535 8.33821C4.80693 8.52399 4.76652 8.7236 4.76652 8.92526C4.76652 9.12691 4.80693 9.32652 4.88535 9.5123C4.96378 9.69808 5.07864 9.86626 5.22314 10.0069C5.51841 10.2943 5.91417 10.4551 6.32619 10.4551C6.73821 10.4551 7.13397 10.2943 7.42924 10.0069L11.7797 5.74017C11.8782 5.64431 12.0102 5.59067 12.1476 5.59067C12.285 5.59067 12.417 5.64431 12.5155 5.74017C12.5637 5.78704 12.602 5.8431 12.6281 5.90503C12.6543 5.96696 12.6678 6.0335 12.6678 6.10073C12.6678 6.16795 12.6543 6.2345 12.6281 6.29643C12.602 6.35835 12.5637 6.41441 12.5155 6.46128L8.1644 10.728C7.67225 11.2067 7.01276 11.4746 6.32619 11.4746C5.63962 11.4746 4.98013 11.2067 4.48798 10.728C4.24701 10.4937 4.05547 10.2134 3.92468 9.90375C3.79389 9.59411 3.7265 9.26139 3.7265 8.92526C3.7265 8.58912 3.79389 8.2564 3.92468 7.94676C4.05547 7.63712 4.24701 7.35684 4.48798 7.1225L8.83845 2.85576C8.93691 2.75989 9.06891 2.70625 9.20633 2.70625C9.34375 2.70625 9.47575 2.75989 9.57422 2.85576C9.62241 2.90263 9.66072 2.95868 9.68688 3.02061C9.71304 3.08254 9.72651 3.14908 9.72651 3.21631C9.72651 3.28353 9.71304 3.35008 9.68688 3.41201Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "Mcp" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/other/Mcp.tsx b/web/app/components/base/icons/src/vender/other/Mcp.tsx new file mode 100644 index 0000000000..00ffa4a831 --- /dev/null +++ b/web/app/components/base/icons/src/vender/other/Mcp.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Mcp.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'Mcp' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json new file mode 100644 index 0000000000..d33d62d344 --- /dev/null +++ b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.json @@ -0,0 +1,279 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "204", + "height": "36", + "viewBox": "0 0 204 36", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.1" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M3.33333 18.3333C3.33333 18.5423 3.64067 18.9056 4.35365 19.2621C5.27603 19.7233 6.58451 20 8 20C9.41547 20 10.7239 19.7233 11.6463 19.2621C12.3593 18.9056 12.6667 18.5423 12.6667 18.3333V16.8858C11.5667 17.5655 9.88487 18 8 18C6.11515 18 4.43331 17.5655 3.33333 16.8858V18.3333ZM12.6667 20.2191C11.5667 20.8988 9.88487 21.3333 8 21.3333C6.11515 21.3333 4.43331 20.8988 3.33333 20.2191V21.6667C3.33333 21.8756 3.64067 22.2389 4.35365 22.5954C5.27603 23.0566 6.58451 23.3333 8 23.3333C9.41547 23.3333 10.7239 23.0566 11.6463 22.5954C12.3593 22.2389 12.6667 21.8756 12.6667 21.6667V20.2191ZM2 21.6667V15C2 13.3431 4.68629 12 8 12C11.3137 12 14 13.3431 14 15V21.6667C14 23.3235 11.3137 24.6667 8 24.6667C4.68629 24.6667 2 23.3235 2 21.6667ZM8 16.6667C9.41547 16.6667 10.7239 16.3899 11.6463 15.9288C12.3593 15.5723 12.6667 15.2089 12.6667 15C12.6667 14.7911 12.3593 14.4277 11.6463 14.0712C10.7239 13.6101 9.41547 13.3333 8 13.3333C6.58451 13.3333 5.27603 13.6101 4.35365 14.0712C3.64067 14.4277 3.33333 14.7911 3.33333 15C3.33333 15.2089 3.64067 15.5723 4.35365 15.9288C5.27603 16.3899 6.58451 16.6667 8 16.6667Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.3" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M41.3337 11.3333C41.7019 11.3333 42.0003 11.6318 42.0003 12V15.3333C42.0003 15.7015 41.7019 16 41.3337 16H38.0003V24.6667C38.0003 25.0349 37.7019 25.3333 37.3337 25.3333H34.667C34.2988 25.3333 34.0003 25.0349 34.0003 24.6667V16H30.3337C29.9655 16 29.667 15.7015 29.667 15.3333V13.7454C29.667 13.4929 29.8097 13.262 30.0355 13.1491L33.667 11.3333H41.3337ZM38.0003 12.6667H33.9818L31.0003 14.1574V14.6667H35.3337V24H36.667V14.6667H38.0003V12.6667ZM40.667 12.6667H39.3337V14.6667H40.667V12.6667Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.6" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M60.6667 13.3333C60.6667 11.8606 61.8606 10.6667 63.3333 10.6667C64.8061 10.6667 66 11.8606 66 13.3333H69.3333C69.7015 13.3333 70 13.6318 70 14V16.7805C70 16.9969 69.8949 17.1998 69.7183 17.3248C69.5415 17.4497 69.3152 17.4811 69.1112 17.409C68.973 17.3602 68.8237 17.3333 68.6667 17.3333C67.9303 17.3333 67.3333 17.9303 67.3333 18.6667C67.3333 19.4031 67.9303 20 68.6667 20C68.8237 20 68.973 19.9731 69.1112 19.9243C69.3152 19.8522 69.5415 19.8836 69.7183 20.0085C69.8949 20.1335 70 20.3365 70 20.5529V23.3333C70 23.7015 69.7015 24 69.3333 24H58.6667C58.2985 24 58 23.7015 58 23.3333V14C58 13.6318 58.2985 13.3333 58.6667 13.3333H60.6667ZM63.3333 12C62.597 12 62 12.5969 62 13.3333C62 13.4903 62.0269 13.6397 62.0757 13.7778C62.1478 13.9819 62.1164 14.2082 61.9915 14.3849C61.8665 14.5616 61.6635 14.6667 61.4471 14.6667H59.3333V22.6667H68.6667V21.3333C67.1939 21.3333 66 20.1394 66 18.6667C66 17.1939 67.1939 16 68.6667 16V14.6667H65.2195C65.0031 14.6667 64.8002 14.5616 64.6752 14.3849C64.5503 14.2082 64.5189 13.9819 64.591 13.7778C64.6398 13.6397 64.6667 13.4904 64.6667 13.3333C64.6667 12.5969 64.0697 12 63.3333 12Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "rect", + "attributes": { + "x": "84.5", + "y": "0.5", + "width": "35", + "height": "35", + "rx": "9.5", + "stroke": "#101828", + "stroke-opacity": "0.04" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M96.167 16.3333H107.834V25.5H96.167V16.3333Z", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M100.333 19.6667H103.666", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M107.833 12.1667L105.583 15.9167", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M97.4888 11.9517C97.5447 11.9238 97.5901 11.8784 97.6181 11.8224L97.9911 11.0765C98.0976 10.8634 98.4017 10.8634 98.5083 11.0765L98.8813 11.8224C98.9092 11.8784 98.9546 11.9238 99.0106 11.9517L99.7565 12.3247C99.9696 12.4313 99.9696 12.7354 99.7565 12.842L99.0106 13.2149C98.9546 13.2429 98.9092 13.2883 98.8813 13.3442L98.5083 14.0902C98.4017 14.3033 98.0976 14.3033 97.9911 14.0902L97.6181 13.3442C97.5901 13.2883 97.5447 13.2429 97.4888 13.2149L96.7429 12.842C96.5297 12.7354 96.5297 12.4313 96.7429 12.3247L97.4888 11.9517Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M101.882 10.5438C101.952 10.5089 102.009 10.4521 102.044 10.3822L102.51 9.4498C102.643 9.1834 103.023 9.1834 103.157 9.4498L103.623 10.3822C103.658 10.4521 103.714 10.5089 103.784 10.5438L104.717 11.0101C104.983 11.1432 104.983 11.5234 104.717 11.6566L103.784 12.1228C103.714 12.1578 103.658 12.2145 103.623 12.2845L103.157 13.2169C103.023 13.4833 102.643 13.4833 102.51 13.2169L102.044 12.2845C102.009 12.2145 101.952 12.1578 101.882 12.1228L100.95 11.6566C100.683 11.5234 100.683 11.1432 100.95 11.0101L101.882 10.5438Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.6", + "clip-path": "url(#clip0_9296_51042)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M145.809 14.7521L145.645 15.1292C145.525 15.4053 145.143 15.4053 145.022 15.1292L144.858 14.7521C144.565 14.0796 144.037 13.5443 143.379 13.2514L142.872 13.0261C142.599 12.9044 142.599 12.5059 142.872 12.3841L143.35 12.1714C144.026 11.871 144.563 11.3158 144.851 10.6206L145.02 10.213C145.138 9.92899 145.53 9.92899 145.647 10.213L145.816 10.6206C146.104 11.3158 146.641 11.871 147.317 12.1714L147.795 12.3841C148.069 12.5059 148.069 12.9044 147.795 13.0261L147.289 13.2514C146.63 13.5443 146.102 14.0796 145.809 14.7521ZM138 11.3333C140.712 11.3333 142.951 13.3571 143.289 15.9766L144.79 18.3358C144.889 18.4911 144.869 18.7231 144.64 18.8211L143.334 19.3807V21.3333C143.334 22.0697 142.737 22.6667 142 22.6667H140.668L140.667 24.6667H134.667L134.667 22.2041C134.667 21.4168 134.376 20.6725 133.837 20.0007C133.105 19.0875 132.667 17.9283 132.667 16.6667C132.667 13.7211 135.055 11.3333 138 11.3333ZM138 12.6667C135.791 12.6667 134 14.4575 134 16.6667C134 17.5899 134.312 18.4619 134.878 19.1666C135.607 20.076 136.001 21.1115 136 22.2042L136 23.3333H139.334L139.335 21.3333H142V18.5013L143.033 18.0587L142.005 16.4417L141.967 16.1475C141.711 14.1676 140.017 12.6667 138 12.6667ZM144.993 21.3286L146.103 22.0683C146.88 20.9041 147.334 19.5051 147.334 18.0001C147.334 17.5447 147.292 17.0991 147.213 16.6667L145.917 17C145.972 17.3252 146 17.6593 146 18.0001C146 19.2314 145.629 20.3761 144.993 21.3286Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.3" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M167.149 16.3522L167.251 16.3822L174.224 19.0391L174.317 19.082C174.722 19.308 174.777 19.8792 174.424 20.179L174.341 20.2389L171.817 21.8171L170.239 24.3418C169.962 24.784 169.324 24.7511 169.082 24.3171L169.039 24.2246L166.382 17.2513C166.188 16.742 166.644 16.2421 167.149 16.3522ZM169.812 22.5085L170.767 20.9811L170.811 20.9186C170.858 20.859 170.916 20.8076 170.981 20.7669L172.508 19.8119L168.152 18.1524L169.812 22.5085Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M165.212 20.3978L163.562 22.0475L162.619 21.1048L164.269 19.4551L165.212 20.3978Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M163.666 18H161.333V16.6667H163.666V18Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M165.212 14.2689L164.269 15.2116L162.619 13.5619L163.562 12.6192L165.212 14.2689Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M172.047 13.5619L170.397 15.2116L169.455 14.2689L171.104 12.6192L172.047 13.5619Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M168 13.6667H166.666V11.3333H168V13.6667Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "g", + "attributes": { + "opacity": "0.1" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M202.666 23.3333V14.6667L201.333 12H190.666L189.333 14.669V23.3333C189.333 23.7015 189.631 24 190 24H202C202.368 24 202.666 23.7015 202.666 23.3333ZM190.666 16H201.333V22.6667H190.666V16ZM191.49 13.3333H200.509L201.176 14.6667H190.824L191.49 13.3333ZM198 17.3333H194V18.6667H198V17.3333Z", + "fill": "currentColor", + "fill-opacity": "0.3" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "clip0_9296_51042" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "16", + "height": "16", + "fill": "white", + "transform": "translate(132 10)" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "NoToolPlaceholder" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.tsx b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.tsx new file mode 100644 index 0000000000..da8fddee22 --- /dev/null +++ b/web/app/components/base/icons/src/vender/other/NoToolPlaceholder.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './NoToolPlaceholder.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'NoToolPlaceholder' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/other/index.ts b/web/app/components/base/icons/src/vender/other/index.ts index 8ddf5e7a86..8a7bb7ae28 100644 --- a/web/app/components/base/icons/src/vender/other/index.ts +++ b/web/app/components/base/icons/src/vender/other/index.ts @@ -1,5 +1,7 @@ export { default as AnthropicText } from './AnthropicText' export { default as Generator } from './Generator' export { default as Group } from './Group' +export { default as Mcp } from './Mcp' +export { default as NoToolPlaceholder } from './NoToolPlaceholder' export { default as Openai } from './Openai' export { default as ReplayLine } from './ReplayLine' diff --git a/web/app/components/base/icons/src/vender/workflow/WindowCursor.json b/web/app/components/base/icons/src/vender/workflow/WindowCursor.json new file mode 100644 index 0000000000..b64ba912bb --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/WindowCursor.json @@ -0,0 +1,62 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M1.33325 4.66663C1.33325 3.56206 2.22869 2.66663 3.33325 2.66663H12.6666C13.7712 2.66663 14.6666 3.56206 14.6666 4.66663V8.16663C14.6666 8.53483 14.3681 8.83329 13.9999 8.83329C13.6317 8.83329 13.3333 8.53483 13.3333 8.16663V4.66663C13.3333 4.29844 13.0348 3.99996 12.6666 3.99996H3.33325C2.96507 3.99996 2.66659 4.29844 2.66659 4.66663V12C2.66659 12.3682 2.96507 12.6666 3.33325 12.6666H7.99992C8.36812 12.6666 8.66658 12.9651 8.66658 13.3333C8.66658 13.7015 8.36812 14 7.99992 14H3.33325C2.22869 14 1.33325 13.1046 1.33325 12V4.66663Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M3.66659 5.83329C3.66659 6.29353 4.03968 6.66663 4.49992 6.66663C4.96016 6.66663 5.33325 6.29353 5.33325 5.83329C5.33325 5.37305 4.96016 4.99996 4.49992 4.99996C4.03968 4.99996 3.66659 5.37305 3.66659 5.83329Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M5.99992 5.83329C5.99992 6.29353 6.37301 6.66663 6.83325 6.66663C7.29352 6.66663 7.66658 6.29353 7.66658 5.83329C7.66658 5.37305 7.29352 4.99996 6.83325 4.99996C6.37301 4.99996 5.99992 5.37305 5.99992 5.83329Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M8.33325 5.83329C8.33325 6.29353 8.70632 6.66663 9.16658 6.66663C9.62685 6.66663 9.99992 6.29353 9.99992 5.83329C9.99992 5.37305 9.62685 4.99996 9.16658 4.99996C8.70632 4.99996 8.33325 5.37305 8.33325 5.83329Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M10.5293 9.69609C10.2933 9.62349 10.0365 9.68729 9.86185 9.86189C9.68725 10.0365 9.62345 10.2934 9.69605 10.5294L11.0294 14.8627C11.1095 15.1231 11.3401 15.3086 11.6116 15.331C11.8832 15.3535 12.1411 15.2085 12.2629 14.9648L13.1635 13.1636L14.9647 12.263C15.2085 12.1411 15.3535 11.8832 15.331 11.6116C15.3085 11.3401 15.1231 11.1096 14.8627 11.0294L10.5293 9.69609Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "WindowCursor" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/workflow/WindowCursor.tsx b/web/app/components/base/icons/src/vender/workflow/WindowCursor.tsx new file mode 100644 index 0000000000..8f48dc0b14 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/WindowCursor.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './WindowCursor.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps<SVGSVGElement> & { + ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>; + }, +) => <IconBase {...props} ref={ref} data={data as IconData} /> + +Icon.displayName = 'WindowCursor' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 7167b71b44..61fbd4b21c 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -19,3 +19,4 @@ export { default as ParameterExtractor } from './ParameterExtractor' export { default as QuestionClassifier } from './QuestionClassifier' export { default as TemplatingTransform } from './TemplatingTransform' export { default as VariableX } from './VariableX' +export { default as WindowCursor } from './WindowCursor' diff --git a/web/app/components/base/input-number/index.spec.tsx b/web/app/components/base/input-number/index.spec.tsx index 8dfd1184b0..891cbd21e3 100644 --- a/web/app/components/base/input-number/index.spec.tsx +++ b/web/app/components/base/input-number/index.spec.tsx @@ -18,7 +18,7 @@ describe('InputNumber Component', () => { it('renders input with default values', () => { render(<InputNumber {...defaultProps} />) - const input = screen.getByRole('textbox') + const input = screen.getByRole('spinbutton') expect(input).toBeInTheDocument() }) @@ -56,7 +56,7 @@ describe('InputNumber Component', () => { it('handles direct input changes', () => { render(<InputNumber {...defaultProps} />) - const input = screen.getByRole('textbox') + const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '42' } }) expect(defaultProps.onChange).toHaveBeenCalledWith(42) @@ -64,7 +64,7 @@ describe('InputNumber Component', () => { it('handles empty input', () => { render(<InputNumber {...defaultProps} value={0} />) - const input = screen.getByRole('textbox') + const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '' } }) expect(defaultProps.onChange).toHaveBeenCalledWith(undefined) @@ -72,7 +72,7 @@ describe('InputNumber Component', () => { it('handles invalid input', () => { render(<InputNumber {...defaultProps} />) - const input = screen.getByRole('textbox') + const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: 'abc' } }) expect(defaultProps.onChange).not.toHaveBeenCalled() @@ -86,7 +86,7 @@ describe('InputNumber Component', () => { it('disables controls when disabled prop is true', () => { render(<InputNumber {...defaultProps} disabled />) - const input = screen.getByRole('textbox') + const input = screen.getByRole('spinbutton') const incrementBtn = screen.getByRole('button', { name: /increment/i }) const decrementBtn = screen.getByRole('button', { name: /decrement/i }) diff --git a/web/app/components/base/input-number/index.tsx b/web/app/components/base/input-number/index.tsx index 98efc94462..5fd45944db 100644 --- a/web/app/components/base/input-number/index.tsx +++ b/web/app/components/base/input-number/index.tsx @@ -55,8 +55,8 @@ export const InputNumber: FC<InputNumberProps> = (props) => { return <div className={classNames('flex', wrapClassName)}> <Input {...rest} // disable default controller - type='text' - className={classNames('rounded-r-none', className)} + type='number' + className={classNames('no-spinner rounded-r-none', className)} value={value} max={max} min={min} @@ -77,8 +77,8 @@ export const InputNumber: FC<InputNumberProps> = (props) => { size={size} /> <div className={classNames( - 'flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs', - disabled && 'opacity-50 cursor-not-allowed', + 'flex flex-col rounded-r-md border-l border-divider-subtle bg-components-input-bg-normal text-text-tertiary focus:shadow-xs', + disabled && 'cursor-not-allowed opacity-50', controlWrapClassName)} > <button diff --git a/web/app/components/base/markdown-blocks/button.tsx b/web/app/components/base/markdown-blocks/button.tsx index 4646b12921..315653bcd0 100644 --- a/web/app/components/base/markdown-blocks/button.tsx +++ b/web/app/components/base/markdown-blocks/button.tsx @@ -14,7 +14,7 @@ const MarkdownButton = ({ node }: any) => { size={size} className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')} onClick={() => { - if (isValidUrl(link)) { + if (link && isValidUrl(link)) { window.open(link, '_blank') return } diff --git a/web/app/components/base/markdown-blocks/code-block.tsx b/web/app/components/base/markdown-blocks/code-block.tsx index 9f8a6a87bb..c88cfde9e6 100644 --- a/web/app/components/base/markdown-blocks/code-block.tsx +++ b/web/app/components/base/markdown-blocks/code-block.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import ReactEcharts from 'echarts-for-react' import SyntaxHighlighter from 'react-syntax-highlighter' import { @@ -62,6 +62,17 @@ const getCorrectCapitalizationLanguageName = (language: string) => { // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message // or use the non-minified dev environment for full errors and additional helpful warnings. +// Define ECharts event parameter types +type EChartsEventParams = { + type: string; + seriesIndex?: number; + dataIndex?: number; + name?: string; + value?: any; + currentIndex?: number; // Added for timeline events + [key: string]: any; +} + const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any) => { const { theme } = useTheme() const [isSVG, setIsSVG] = useState(true) @@ -70,6 +81,11 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any const echartsRef = useRef<any>(null) const contentRef = useRef<string>('') const processedRef = useRef<boolean>(false) // Track if content was successfully processed + const instanceIdRef = useRef<string>(`chart-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`) // Unique ID for logging + const isInitialRenderRef = useRef<boolean>(true) // Track if this is initial render + const chartInstanceRef = useRef<any>(null) // Direct reference to ECharts instance + const resizeTimerRef = useRef<NodeJS.Timeout | null>(null) // For debounce handling + const finishedEventCountRef = useRef<number>(0) // Track finished event trigger count const match = /language-(\w+)/.exec(className || '') const language = match?.[1] const languageShowName = getCorrectCapitalizationLanguageName(language || '') @@ -85,36 +101,64 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any width: 'auto', }) as any, []) - const echartsOnEvents = useMemo(() => ({ - finished: () => { - const instance = echartsRef.current?.getEchartsInstance?.() - if (instance) - instance.resize() + // Debounce resize operations + const debouncedResize = useCallback(() => { + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) + + resizeTimerRef.current = setTimeout(() => { + if (chartInstanceRef.current) + chartInstanceRef.current.resize() + resizeTimerRef.current = null + }, 200) + }, []) + + // Handle ECharts instance initialization + const handleChartReady = useCallback((instance: any) => { + chartInstanceRef.current = instance + + // Force resize to ensure timeline displays correctly + setTimeout(() => { + if (chartInstanceRef.current) + chartInstanceRef.current.resize() + }, 200) + }, []) + + // Store event handlers in useMemo to avoid recreating them + const echartsEvents = useMemo(() => ({ + finished: (params: EChartsEventParams) => { + // Limit finished event frequency to avoid infinite loops + finishedEventCountRef.current++ + if (finishedEventCountRef.current > 3) { + // Stop processing after 3 times to avoid infinite loops + return + } + + if (chartInstanceRef.current) { + // Use debounced resize + debouncedResize() + } }, - }), [echartsRef]) // echartsRef is stable, so this effectively runs once. + }), [debouncedResize]) // Handle container resize for echarts useEffect(() => { - if (language !== 'echarts' || !echartsRef.current) return + if (language !== 'echarts' || !chartInstanceRef.current) return const handleResize = () => { - // This gets the echarts instance from the component - const instance = echartsRef.current?.getEchartsInstance?.() - if (instance) - instance.resize() + if (chartInstanceRef.current) + // Use debounced resize + debouncedResize() } window.addEventListener('resize', handleResize) - // Also manually trigger resize after a short delay to ensure proper sizing - const resizeTimer = setTimeout(handleResize, 200) - return () => { window.removeEventListener('resize', handleResize) - clearTimeout(resizeTimer) + if (resizeTimerRef.current) + clearTimeout(resizeTimerRef.current) } - }, [language, echartsRef.current]) - + }, [language, debouncedResize]) // Process chart data when content changes useEffect(() => { // Only process echarts content @@ -222,13 +266,12 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any } }, [language, children]) + // Cache rendered content to avoid unnecessary re-renders const renderCodeContent = useMemo(() => { const content = String(children).replace(/\n$/, '') switch (language) { case 'mermaid': - if (isSVG) - return <Flowchart PrimitiveCode={content} /> - break + return <Flowchart PrimitiveCode={content} theme={theme as 'light' | 'dark'} /> case 'echarts': { // Loading state: show loading indicator if (chartState === 'loading') { @@ -274,6 +317,9 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any // Success state: show the chart if (chartState === 'success' && finalChartOption) { + // Reset finished event counter + finishedEventCountRef.current = 0 + return ( <div style={{ minWidth: '300px', @@ -286,13 +332,20 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any }}> <ErrorBoundary> <ReactEcharts - ref={echartsRef} + ref={(e) => { + if (e && isInitialRenderRef.current) { + echartsRef.current = e + isInitialRenderRef.current = false + } + }} option={finalChartOption} style={echartsStyle} theme={isDarkMode ? 'dark' : undefined} opts={echartsOpts} - notMerge={true} - onEvents={echartsOnEvents} + notMerge={false} + lazyUpdate={false} + onEvents={echartsEvents} + onChartReady={handleChartReady} /> </ErrorBoundary> </div> @@ -363,7 +416,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any </SyntaxHighlighter> ) } - }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, echartsOnEvents]) + }, [children, language, isSVG, finalChartOption, props, theme, match, chartState, isDarkMode, echartsStyle, echartsOpts, handleChartReady, echartsEvents]) if (inline || !match) return <code {...props} className={className}>{children}</code> @@ -373,7 +426,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any <div className='flex h-8 items-center justify-between rounded-t-[10px] border-b border-divider-subtle bg-components-input-bg-normal p-1 pl-3'> <div className='system-xs-semibold-uppercase text-text-secondary'>{languageShowName}</div> <div className='flex items-center gap-1'> - {(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />} + {language === 'svg' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />} <ActionButton> <CopyIcon content={String(children).replace(/\n$/, '')} /> </ActionButton> diff --git a/web/app/components/base/markdown-blocks/form.tsx b/web/app/components/base/markdown-blocks/form.tsx index ab7e7cef53..b71193d8f9 100644 --- a/web/app/components/base/markdown-blocks/form.tsx +++ b/web/app/components/base/markdown-blocks/form.tsx @@ -28,6 +28,7 @@ enum SUPPORTED_TYPES { DATETIME = 'datetime', CHECKBOX = 'checkbox', SELECT = 'select', + HIDDEN = 'hidden', } const MarkdownForm = ({ node }: any) => { const { onSend } = useChatContext() @@ -37,8 +38,12 @@ const MarkdownForm = ({ node }: any) => { useEffect(() => { const initialValues: { [key: string]: any } = {} node.children.forEach((child: any) => { - if ([SUPPORTED_TAGS.INPUT, SUPPORTED_TAGS.TEXTAREA].includes(child.tagName)) - initialValues[child.properties.name] = child.properties.value + if ([SUPPORTED_TAGS.INPUT, SUPPORTED_TAGS.TEXTAREA].includes(child.tagName)) { + initialValues[child.properties.name] + = (child.tagName === SUPPORTED_TAGS.INPUT && child.properties.type === SUPPORTED_TYPES.HIDDEN) + ? (child.properties.value || '') + : child.properties.value + } }) setFormValues(initialValues) }, [node.children]) @@ -180,6 +185,17 @@ const MarkdownForm = ({ node }: any) => { ) } + if (child.properties.type === SUPPORTED_TYPES.HIDDEN) { + return ( + <input + key={index} + type="hidden" + name={child.properties.name} + value={formValues[child.properties.name] || child.properties.value || ''} + /> + ) + } + return ( <Input key={index} diff --git a/web/app/components/base/markdown-blocks/link.tsx b/web/app/components/base/markdown-blocks/link.tsx index c465b3e4f8..458d455516 100644 --- a/web/app/components/base/markdown-blocks/link.tsx +++ b/web/app/components/base/markdown-blocks/link.tsx @@ -16,7 +16,7 @@ const Link = ({ node, children, ...props }: any) => { } else { const href = props.href || node.properties?.href - if(!isValidUrl(href)) + if(!href || !isValidUrl(href)) return <span>{children}</span> return <a href={href} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a> diff --git a/web/app/components/base/markdown-blocks/think-block.tsx b/web/app/components/base/markdown-blocks/think-block.tsx index 565582e326..46f992d758 100644 --- a/web/app/components/base/markdown-blocks/think-block.tsx +++ b/web/app/components/base/markdown-blocks/think-block.tsx @@ -71,7 +71,7 @@ export const ThinkBlock = ({ children, ...props }: any) => { return ( <details {...(!isComplete && { open: true })} className="group"> - <summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-gray-500"> + <summary className="flex cursor-pointer select-none list-none items-center whitespace-nowrap pl-2 font-bold text-text-secondary"> <div className="flex shrink-0 items-center"> <svg className="mr-2 h-3 w-3 transition-transform duration-500 group-open:rotate-90" @@ -89,7 +89,7 @@ export const ThinkBlock = ({ children, ...props }: any) => { {isComplete ? `${t('common.chat.thought')}(${elapsedTime.toFixed(1)}s)` : `${t('common.chat.thinking')}(${elapsedTime.toFixed(1)}s)`} </div> </summary> - <div className="ml-2 border-l border-gray-300 bg-gray-50 p-3 text-gray-500"> + <div className="ml-2 border-l border-components-panel-border bg-components-panel-bg-alt p-3 text-text-secondary"> {displayContent} </div> </details> diff --git a/web/app/components/base/markdown-blocks/utils.ts b/web/app/components/base/markdown-blocks/utils.ts index 4e9e98dbed..d8df76aefc 100644 --- a/web/app/components/base/markdown-blocks/utils.ts +++ b/web/app/components/base/markdown-blocks/utils.ts @@ -1,3 +1,7 @@ +import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config' + export const isValidUrl = (url: string): boolean => { - return ['http:', 'https:', '//', 'mailto:'].some(prefix => url.startsWith(prefix)) + const validPrefixes = ['http:', 'https:', '//', 'mailto:'] + if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:') + return validPrefixes.some(prefix => url.startsWith(prefix)) } diff --git a/web/app/components/base/markdown/markdown-utils.ts b/web/app/components/base/markdown/markdown-utils.ts index 0aa385a1d1..0089bef0ac 100644 --- a/web/app/components/base/markdown/markdown-utils.ts +++ b/web/app/components/base/markdown/markdown-utils.ts @@ -4,6 +4,7 @@ * Includes preprocessing for LaTeX and custom "think" tags. */ import { flow } from 'lodash-es' +import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config' export const preprocessLaTeX = (content: string) => { if (typeof content !== 'string') @@ -11,6 +12,7 @@ export const preprocessLaTeX = (content: string) => { const codeBlockRegex = /```[\s\S]*?```/g const codeBlocks = content.match(codeBlockRegex) || [] + const escapeReplacement = (str: string) => str.replace(/\$/g, '_TMP_REPLACE_DOLLAR_') let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER') processedContent = flow([ @@ -21,14 +23,16 @@ export const preprocessLaTeX = (content: string) => { ])(processedContent) codeBlocks.forEach((block) => { - processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block) + processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', escapeReplacement(block)) }) + processedContent = processedContent.replace(/_TMP_REPLACE_DOLLAR_/g, '$') + return processedContent } export const preprocessThinkTag = (content: string) => { - const thinkOpenTagRegex = /<think>\n/g + const thinkOpenTagRegex = /(<think>\n)+/g const thinkCloseTagRegex = /\n<\/think>/g return flow([ (str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'), @@ -83,5 +87,8 @@ export const customUrlTransform = (uri: string): string | undefined => { if (PERMITTED_SCHEME_REGEX.test(scheme)) return uri + if (ALLOW_UNSAFE_DATA_SCHEME && scheme === 'data:') + return uri + return undefined } diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx index 31eaffb813..a953ef15a8 100644 --- a/web/app/components/base/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import mermaid from 'mermaid' +import mermaid, { type MermaidConfig } from 'mermaid' import { useTranslation } from 'react-i18next' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { MoonIcon, SunIcon } from '@heroicons/react/24/solid' @@ -68,14 +68,13 @@ const THEMES = { const initMermaid = () => { if (typeof window !== 'undefined' && !isMermaidInitialized) { try { - mermaid.initialize({ + const config: MermaidConfig = { startOnLoad: false, fontFamily: 'sans-serif', securityLevel: 'loose', flowchart: { htmlLabels: true, useMaxWidth: true, - diagramPadding: 10, curve: 'basis', nodeSpacing: 50, rankSpacing: 70, @@ -94,10 +93,10 @@ const initMermaid = () => { mindmap: { useMaxWidth: true, padding: 10, - diagramPadding: 20, }, maxTextSize: 50000, - }) + } + mermaid.initialize(config) isMermaidInitialized = true } catch (error) { @@ -113,7 +112,7 @@ const Flowchart = React.forwardRef((props: { theme?: 'light' | 'dark' }, ref) => { const { t } = useTranslation() - const [svgCode, setSvgCode] = useState<string | null>(null) + const [svgString, setSvgString] = useState<string | null>(null) const [look, setLook] = useState<'classic' | 'handDrawn'>('classic') const [isInitialized, setIsInitialized] = useState(false) const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light') @@ -125,6 +124,7 @@ const Flowchart = React.forwardRef((props: { const [imagePreviewUrl, setImagePreviewUrl] = useState('') const [isCodeComplete, setIsCodeComplete] = useState(false) const codeCompletionCheckRef = useRef<NodeJS.Timeout>() + const prevCodeRef = useRef<string>() // Create cache key from code, style and theme const cacheKey = useMemo(() => { @@ -169,50 +169,18 @@ const Flowchart = React.forwardRef((props: { */ const handleRenderError = (error: any) => { console.error('Mermaid rendering error:', error) - const errorMsg = (error as Error).message - if (errorMsg.includes('getAttribute')) { - diagramCache.clear() - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'loose', - }) - } - else { - setErrMsg(`Rendering chart failed, please refresh and try again ${look === 'handDrawn' ? 'Or try using classic mode' : ''}`) + // On any render error, assume the mermaid state is corrupted and force a re-initialization. + try { + diagramCache.clear() // Clear cache to prevent using potentially corrupted SVGs + isMermaidInitialized = false // <-- THE FIX: Force re-initialization + initMermaid() // Re-initialize with the default safe configuration } - - if (look === 'handDrawn') { - try { - // Clear possible cache issues - diagramCache.delete(`${props.PrimitiveCode}-handDrawn-${currentTheme}`) - - // Reset mermaid configuration - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'loose', - theme: 'default', - maxTextSize: 50000, - }) - - // Try rendering with standard mode - setLook('classic') - setErrMsg('Hand-drawn mode is not supported for this diagram. Switched to classic mode.') - - // Delay error clearing - setTimeout(() => { - if (containerRef.current) { - // Try rendering again with standard mode, but can't call renderFlowchart directly due to circular dependency - // Instead set state to trigger re-render - setIsCodeComplete(true) // This will trigger useEffect re-render - } - }, 500) - } - catch (e) { - console.error('Reset after handDrawn error failed:', e) - } + catch (reinitError) { + console.error('Failed to re-initialize Mermaid after error:', reinitError) } + setErrMsg(`Rendering failed: ${(error as Error).message || 'Unknown error. Please check the console.'}`) setIsLoading(false) } @@ -223,51 +191,23 @@ const Flowchart = React.forwardRef((props: { setIsInitialized(true) }, []) - // Update theme when prop changes + // Update theme when prop changes, but allow internal override. + const prevThemeRef = useRef<string>() useEffect(() => { - if (props.theme) + // Only react if the theme prop from the outside has actually changed. + if (props.theme && props.theme !== prevThemeRef.current) { + // When the global theme prop changes, it should act as the source of truth, + // overriding any local theme selection. + diagramCache.clear() + setSvgString(null) setCurrentTheme(props.theme) - }, [props.theme]) - - // Validate mermaid code and check for completeness - useEffect(() => { - if (codeCompletionCheckRef.current) - clearTimeout(codeCompletionCheckRef.current) - - // Reset code complete status when code changes - setIsCodeComplete(false) - - // If no code or code is extremely short, don't proceed - if (!props.PrimitiveCode || props.PrimitiveCode.length < 10) - return - - // Check if code already in cache - if so we know it's valid - if (diagramCache.has(cacheKey)) { - setIsCodeComplete(true) - return - } - - // Initial check using the extracted isMermaidCodeComplete function - const isComplete = isMermaidCodeComplete(props.PrimitiveCode) - if (isComplete) { - setIsCodeComplete(true) - return + // Reset look to classic for a consistent state after a global change. + setLook('classic') } + // Update the ref to the current prop value for the next render. + prevThemeRef.current = props.theme + }, [props.theme]) - // Set a delay to check again in case code is still being generated - codeCompletionCheckRef.current = setTimeout(() => { - setIsCodeComplete(isMermaidCodeComplete(props.PrimitiveCode)) - }, 300) - - return () => { - if (codeCompletionCheckRef.current) - clearTimeout(codeCompletionCheckRef.current) - } - }, [props.PrimitiveCode, cacheKey]) - - /** - * Renders flowchart based on provided code - */ const renderFlowchart = useCallback(async (primitiveCode: string) => { if (!isInitialized || !containerRef.current) { setIsLoading(false) @@ -275,15 +215,11 @@ const Flowchart = React.forwardRef((props: { return } - // Don't render if code is not complete yet - if (!isCodeComplete) { - setIsLoading(true) - return - } - // Return cached result if available + const cacheKey = `${primitiveCode}-${look}-${currentTheme}` if (diagramCache.has(cacheKey)) { - setSvgCode(diagramCache.get(cacheKey) || null) + setErrMsg('') + setSvgString(diagramCache.get(cacheKey) || null) setIsLoading(false) return } @@ -294,17 +230,45 @@ const Flowchart = React.forwardRef((props: { try { let finalCode: string - // Check if it's a gantt chart or mindmap - const isGanttChart = primitiveCode.trim().startsWith('gantt') - const isMindMap = primitiveCode.trim().startsWith('mindmap') - - if (isGanttChart || isMindMap) { - // For gantt charts and mindmaps, ensure each task is on its own line - // and preserve exact whitespace/format - finalCode = primitiveCode.trim() + const trimmedCode = primitiveCode.trim() + const isGantt = trimmedCode.startsWith('gantt') + const isMindMap = trimmedCode.startsWith('mindmap') + const isSequence = trimmedCode.startsWith('sequenceDiagram') + + if (isGantt || isMindMap || isSequence) { + if (isGantt) { + finalCode = trimmedCode + .split('\n') + .map((line) => { + // Gantt charts have specific syntax needs. + const taskMatch = line.match(/^\s*([^:]+?)\s*:\s*(.*)/) + if (!taskMatch) + return line // Not a task line, return as is. + + const taskName = taskMatch[1].trim() + let paramsStr = taskMatch[2].trim() + + // Rule 1: Correct multiple "after" dependencies ONLY if they exist. + // This is a common mistake, e.g., "..., after task1, after task2, ..." + const afterCount = (paramsStr.match(/after /g) || []).length + if (afterCount > 1) + paramsStr = paramsStr.replace(/,\s*after\s+/g, ' ') + + // Rule 2: Normalize spacing between parameters for consistency. + const finalParams = paramsStr.replace(/\s*,\s*/g, ', ').trim() + return `${taskName} :${finalParams}` + }) + .join('\n') + } + else { + // For mindmap and sequence charts, which are sensitive to syntax, + // pass the code through directly. + finalCode = trimmedCode + } } else { // Step 1: Clean and prepare Mermaid code using the extracted prepareMermaidCode function + // This function handles flowcharts appropriately. finalCode = prepareMermaidCode(primitiveCode, look) } @@ -319,13 +283,12 @@ const Flowchart = React.forwardRef((props: { THEMES, ) - // Step 4: Clean SVG code and convert to base64 using the extracted functions + // Step 4: Clean up SVG code const cleanedSvg = cleanUpSvgCode(processedSvg) - const base64Svg = await svgToBase64(cleanedSvg) - if (base64Svg && typeof base64Svg === 'string') { - diagramCache.set(cacheKey, base64Svg) - setSvgCode(base64Svg) + if (cleanedSvg && typeof cleanedSvg === 'string') { + diagramCache.set(cacheKey, cleanedSvg) + setSvgString(cleanedSvg) } setIsLoading(false) @@ -334,12 +297,9 @@ const Flowchart = React.forwardRef((props: { // Error handling handleRenderError(error) } - }, [chartId, isInitialized, cacheKey, isCodeComplete, look, currentTheme, t]) + }, [chartId, isInitialized, look, currentTheme, t]) - /** - * Configure mermaid based on selected style and theme - */ - const configureMermaid = useCallback(() => { + const configureMermaid = useCallback((primitiveCode: string) => { if (typeof window !== 'undefined' && isInitialized) { const themeVars = THEMES[currentTheme] const config: any = { @@ -361,23 +321,37 @@ const Flowchart = React.forwardRef((props: { mindmap: { useMaxWidth: true, padding: 10, - diagramPadding: 20, }, } + const isFlowchart = primitiveCode.trim().startsWith('graph') || primitiveCode.trim().startsWith('flowchart') + if (look === 'classic') { config.theme = currentTheme === 'dark' ? 'dark' : 'neutral' - config.flowchart = { - htmlLabels: true, - useMaxWidth: true, - diagramPadding: 12, - nodeSpacing: 60, - rankSpacing: 80, - curve: 'linear', - ranker: 'tight-tree', + + if (isFlowchart) { + config.flowchart = { + htmlLabels: true, + useMaxWidth: true, + nodeSpacing: 60, + rankSpacing: 80, + curve: 'linear', + ranker: 'tight-tree', + } + } + + if (currentTheme === 'dark') { + config.themeVariables = { + background: themeVars.background, + primaryColor: themeVars.primaryColor, + primaryBorderColor: themeVars.primaryBorderColor, + primaryTextColor: themeVars.primaryTextColor, + secondaryColor: themeVars.secondaryColor, + tertiaryColor: themeVars.tertiaryColor, + } } } - else { + else { // look === 'handDrawn' config.theme = 'default' config.themeCSS = ` .node rect { fill-opacity: 0.85; } @@ -389,27 +363,17 @@ const Flowchart = React.forwardRef((props: { config.themeVariables = { fontSize: '14px', fontFamily: 'sans-serif', + primaryBorderColor: currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor, } - config.flowchart = { - htmlLabels: true, - useMaxWidth: true, - diagramPadding: 10, - nodeSpacing: 40, - rankSpacing: 60, - curve: 'basis', - } - config.themeVariables.primaryBorderColor = currentTheme === 'dark' ? THEMES.dark.connectionColor : THEMES.light.connectionColor - } - if (currentTheme === 'dark' && !config.themeVariables) { - config.themeVariables = { - background: themeVars.background, - primaryColor: themeVars.primaryColor, - primaryBorderColor: themeVars.primaryBorderColor, - primaryTextColor: themeVars.primaryTextColor, - secondaryColor: themeVars.secondaryColor, - tertiaryColor: themeVars.tertiaryColor, - fontFamily: 'sans-serif', + if (isFlowchart) { + config.flowchart = { + htmlLabels: true, + useMaxWidth: true, + nodeSpacing: 40, + rankSpacing: 60, + curve: 'basis', + } } } @@ -425,44 +389,50 @@ const Flowchart = React.forwardRef((props: { return false }, [currentTheme, isInitialized, look]) - // Effect for theme and style configuration + // This is the main rendering effect. + // It triggers whenever the code, theme, or style changes. useEffect(() => { - if (diagramCache.has(cacheKey)) { - setSvgCode(diagramCache.get(cacheKey) || null) - setIsLoading(false) + if (!isInitialized) return - } - - if (configureMermaid() && containerRef.current && isCodeComplete) - renderFlowchart(props.PrimitiveCode) - }, [look, props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, currentTheme, isCodeComplete, configureMermaid]) - // Effect for rendering with debounce - useEffect(() => { - if (diagramCache.has(cacheKey)) { - setSvgCode(diagramCache.get(cacheKey) || null) + // Don't render if code is too short + if (!props.PrimitiveCode || props.PrimitiveCode.length < 10) { setIsLoading(false) + setSvgString(null) return } + // Use a timeout to handle streaming code and debounce rendering if (renderTimeoutRef.current) clearTimeout(renderTimeoutRef.current) - if (isCodeComplete) { - renderTimeoutRef.current = setTimeout(() => { - if (isInitialized) - renderFlowchart(props.PrimitiveCode) - }, 300) - } - else { - setIsLoading(true) - } + setIsLoading(true) + + renderTimeoutRef.current = setTimeout(() => { + // Final validation before rendering + if (!isMermaidCodeComplete(props.PrimitiveCode)) { + setIsLoading(false) + setErrMsg('Diagram code is not complete or invalid.') + return + } + + const cacheKey = `${props.PrimitiveCode}-${look}-${currentTheme}` + if (diagramCache.has(cacheKey)) { + setErrMsg('') + setSvgString(diagramCache.get(cacheKey) || null) + setIsLoading(false) + return + } + + if (configureMermaid(props.PrimitiveCode)) + renderFlowchart(props.PrimitiveCode) + }, 300) // 300ms debounce return () => { if (renderTimeoutRef.current) clearTimeout(renderTimeoutRef.current) } - }, [props.PrimitiveCode, renderFlowchart, isInitialized, cacheKey, isCodeComplete]) + }, [props.PrimitiveCode, look, currentTheme, isInitialized, configureMermaid, renderFlowchart]) // Cleanup on unmount useEffect(() => { @@ -471,14 +441,22 @@ const Flowchart = React.forwardRef((props: { containerRef.current.innerHTML = '' if (renderTimeoutRef.current) clearTimeout(renderTimeoutRef.current) - if (codeCompletionCheckRef.current) - clearTimeout(codeCompletionCheckRef.current) } }, []) + const handlePreviewClick = async () => { + if (svgString) { + const base64 = await svgToBase64(svgString) + setImagePreviewUrl(base64) + } + } + const toggleTheme = () => { - setCurrentTheme(prevTheme => prevTheme === 'light' ? Theme.dark : Theme.light) + const newTheme = currentTheme === 'light' ? 'dark' : 'light' + // Ensure a full, clean re-render cycle, consistent with global theme change. diagramCache.clear() + setSvgString(null) + setCurrentTheme(newTheme) } // Style classes for theme-dependent elements @@ -527,14 +505,26 @@ const Flowchart = React.forwardRef((props: { <div key='classic' className={getLookButtonClass('classic')} - onClick={() => setLook('classic')} + onClick={() => { + if (look !== 'classic') { + diagramCache.clear() + setSvgString(null) + setLook('classic') + } + }} > <div className="msh-segmented-item-label">{t('app.mermaid.classic')}</div> </div> <div key='handDrawn' className={getLookButtonClass('handDrawn')} - onClick={() => setLook('handDrawn')} + onClick={() => { + if (look !== 'handDrawn') { + diagramCache.clear() + setSvgString(null) + setLook('handDrawn') + } + }} > <div className="msh-segmented-item-label">{t('app.mermaid.handDrawn')}</div> </div> @@ -544,7 +534,7 @@ const Flowchart = React.forwardRef((props: { <div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} /> - {isLoading && !svgCode && ( + {isLoading && !svgString && ( <div className='px-[26px] py-4'> <LoadingAnim type='text'/> {!isCodeComplete && ( @@ -555,8 +545,8 @@ const Flowchart = React.forwardRef((props: { </div> )} - {svgCode && ( - <div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}> + {svgString && ( + <div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={handlePreviewClick}> <div className="absolute bottom-2 left-2 z-[100]"> <button onClick={(e) => { @@ -571,11 +561,9 @@ const Flowchart = React.forwardRef((props: { </button> </div> - <img - src={svgCode} - alt="mermaid_chart" + <div style={{ maxWidth: '100%' }} - onError={() => { setErrMsg('Chart rendering failed, please refresh and retry') }} + dangerouslySetInnerHTML={{ __html: svgString }} /> </div> )} diff --git a/web/app/components/base/mermaid/utils.ts b/web/app/components/base/mermaid/utils.ts index 9936a9fc59..9d56494227 100644 --- a/web/app/components/base/mermaid/utils.ts +++ b/web/app/components/base/mermaid/utils.ts @@ -3,52 +3,31 @@ export function cleanUpSvgCode(svgCode: string): string { } /** - * Preprocesses mermaid code to fix common syntax issues + * Prepares mermaid code for rendering by sanitizing common syntax issues. + * @param {string} mermaidCode - The mermaid code to prepare + * @param {'classic' | 'handDrawn'} style - The rendering style + * @returns {string} - The prepared mermaid code */ -export function preprocessMermaidCode(code: string): string { - if (!code || typeof code !== 'string') +export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'handDrawn'): string => { + if (!mermaidCode || typeof mermaidCode !== 'string') return '' - // First check if this is a gantt chart - if (code.trim().startsWith('gantt')) { - // For gantt charts, we need to ensure each task is on its own line - // Split the code into lines and process each line separately - const lines = code.split('\n').map(line => line.trim()) - return lines.join('\n') - } + let code = mermaidCode.trim() - return code - // Replace English colons with Chinese colons in section nodes to avoid parsing issues - .replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}:`) - // Fix common syntax issues - .replace(/fifopacket/g, 'rect') - // Ensure graph has direction - .replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => { - return direction ? match : 'graph TD' - }) - // Clean up empty lines and extra spaces - .trim() -} + // Security: Sanitize against javascript: protocol in click events (XSS vector) + code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2') -/** - * Prepares mermaid code based on selected style - */ -export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string { - let finalCode = preprocessMermaidCode(code) + // Convenience: Basic BR replacement. This is a common and safe operation. + code = code.replace(/<br\s*\/?>/g, '\n') - // Special handling for gantt charts and mindmaps - if (finalCode.trim().startsWith('gantt') || finalCode.trim().startsWith('mindmap')) { - // For gantt charts and mindmaps, preserve the structure exactly as is - return finalCode - } + let finalCode = code + // Hand-drawn style requires some specific clean-up. if (style === 'handDrawn') { finalCode = finalCode - // Remove style definitions that interfere with hand-drawn style .replace(/style\s+[^\n]+/g, '') .replace(/linkStyle\s+[^\n]+/g, '') .replace(/^flowchart/, 'graph') - // Remove any styles that might interfere with hand-drawn style .replace(/class="[^"]*"/g, '') .replace(/fill="[^"]*"/g, '') .replace(/stroke="[^"]*"/g, '') @@ -82,7 +61,6 @@ export function svgToBase64(svgGraph: string): Promise<string> { }) } catch (error) { - console.error('Error converting SVG to base64:', error) return Promise.resolve('') } } @@ -115,13 +93,11 @@ export function processSvgForTheme( } else { let i = 0 - themes.dark.nodeColors.forEach(() => { - const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g - processedSvg = processedSvg.replace(regex, (match: string) => { - const colorIndex = i % themes.dark.nodeColors.length - i++ - return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`) - }) + const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g + processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => { + const colorIndex = i % themes.dark.nodeColors.length + i++ + return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.dark.nodeColors[colorIndex].bg}"`) }) processedSvg = processedSvg @@ -139,14 +115,12 @@ export function processSvgForTheme( .replace(/stroke-width="1"/g, 'stroke-width="1.5"') } else { - themes.light.nodeColors.forEach(() => { - const regex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g - let i = 0 - processedSvg = processedSvg.replace(regex, (match: string) => { - const colorIndex = i % themes.light.nodeColors.length - i++ - return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`) - }) + let i = 0 + const nodeColorRegex = /fill="#[a-fA-F0-9]{6}"[^>]*class="node-[^"]*"/g + processedSvg = processedSvg.replace(nodeColorRegex, (match: string) => { + const colorIndex = i % themes.light.nodeColors.length + i++ + return match.replace(/fill="#[a-fA-F0-9]{6}"/, `fill="${themes.light.nodeColors[colorIndex].bg}"`) }) processedSvg = processedSvg @@ -187,24 +161,10 @@ export function isMermaidCodeComplete(code: string): boolean { // Check for basic syntax structure const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram|mindmap)/.test(trimmedCode) - // Check for balanced brackets and parentheses - const isBalanced = (() => { - const stack = [] - const pairs = { '{': '}', '[': ']', '(': ')' } - - for (const char of trimmedCode) { - if (char in pairs) { - stack.push(char) - } - else if (Object.values(pairs).includes(char)) { - const last = stack.pop() - if (pairs[last as keyof typeof pairs] !== char) - return false - } - } - - return stack.length === 0 - })() + // The balanced bracket check was too strict and produced false negatives for valid + // mermaid syntax like the asymmetric shape `A>B]`. Relying on Mermaid's own + // parser is more robust. + const isBalanced = true // Check for common syntax errors const hasNoSyntaxErrors = !trimmedCode.includes('undefined') @@ -215,7 +175,7 @@ export function isMermaidCodeComplete(code: string): boolean { return hasValidStart && isBalanced && hasNoSyntaxErrors } catch (error) { - console.debug('Mermaid code validation error:', error) + console.error('Mermaid code validation error:', error) return false } } diff --git a/web/app/components/base/popover/index.tsx b/web/app/components/base/popover/index.tsx index 2a831e0c24..0e7c384564 100644 --- a/web/app/components/base/popover/index.tsx +++ b/web/app/components/base/popover/index.tsx @@ -3,6 +3,7 @@ import { Fragment, cloneElement, useRef } from 'react' import cn from '@/utils/classnames' export type HtmlContentProps = { + open?: boolean onClose?: () => void onClick?: () => void } @@ -100,7 +101,8 @@ export default function CustomPopover({ } > {cloneElement(htmlContent as React.ReactElement, { - onClose: () => onMouseLeave(open), + open, + onClose: close, ...(manualClose ? { onClick: close, diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 94a65e4b62..a87a51cd50 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -64,8 +64,9 @@ import cn from '@/utils/classnames' export type PromptEditorProps = { instanceId?: string compact?: boolean + wrapperClassName?: string className?: string - placeholder?: string + placeholder?: string | JSX.Element placeholderClassName?: string style?: React.CSSProperties value?: string @@ -85,6 +86,7 @@ export type PromptEditorProps = { const PromptEditor: FC<PromptEditorProps> = ({ instanceId, compact, + wrapperClassName, className, placeholder, placeholderClassName, @@ -147,10 +149,25 @@ const PromptEditor: FC<PromptEditorProps> = ({ return ( <LexicalComposer initialConfig={{ ...initialConfig, editable }}> - <div className='relative min-h-5'> + <div className={cn('relative', wrapperClassName)}> <RichTextPlugin - contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'text-[13px] leading-5' : 'text-sm leading-6'} text-text-secondary`} style={style || {}} />} - placeholder={<Placeholder value={placeholder} className={cn('truncate', placeholderClassName)} compact={compact} />} + contentEditable={ + <ContentEditable + className={cn( + 'text-text-secondary outline-none', + compact ? 'text-[13px] leading-5' : 'text-sm leading-6', + className, + )} + style={style || {}} + /> + } + placeholder={ + <Placeholder + value={placeholder} + className={cn('truncate', placeholderClassName)} + compact={compact} + /> + } ErrorBoundary={LexicalErrorBoundary} /> <ComponentPickerBlock diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index b43d2c8117..bffcdc60d2 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -165,6 +165,7 @@ const ComponentPicker = ({ isSupportFileVar={isSupportFileVar} onClose={handleClose} onBlur={handleClose} + autoFocus={false} /> </div> ) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx index 55be7810a8..f1a4aff3c4 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.tsx @@ -32,6 +32,10 @@ export const PromptMenuItem = memo(({ return onMouseEnter() }} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + }} onClick={() => { if (disabled) return diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx index 20c03768d0..57de34a701 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.tsx @@ -44,6 +44,10 @@ export const VariableMenuItem = memo(({ tabIndex={-1} ref={setRefElement} onMouseEnter={onMouseEnter} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + }} onClick={onClick}> <div className='mr-2'> {icon} diff --git a/web/app/components/base/prompt-editor/plugins/custom-text/node.tsx b/web/app/components/base/prompt-editor/plugins/custom-text/node.tsx index 49f4a056db..29c2d4ddb4 100644 --- a/web/app/components/base/prompt-editor/plugins/custom-text/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/custom-text/node.tsx @@ -16,7 +16,6 @@ export class CustomTextNode extends TextNode { createDOM(config: EditorConfig) { const dom = super.createDOM(config) - dom.classList.add('align-middle') return dom } diff --git a/web/app/components/base/prompt-editor/plugins/placeholder.tsx b/web/app/components/base/prompt-editor/plugins/placeholder.tsx index 8b869139a9..5e2227dabb 100644 --- a/web/app/components/base/prompt-editor/plugins/placeholder.tsx +++ b/web/app/components/base/prompt-editor/plugins/placeholder.tsx @@ -8,7 +8,7 @@ const Placeholder = ({ className, }: { compact?: boolean - value?: string + value?: string | JSX.Element className?: string }) => { const { t } = useTranslation() diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 731841f423..da5ad84cb1 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -32,12 +32,14 @@ import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from '@/app/components/workflow/nodes/_base/components/variable/var-full-path-panel' import { Type } from '@/app/components/workflow/nodes/llm/types' -import type { ValueSelector } from '@/app/components/workflow/types' +import type { ValueSelector, Var } from '@/app/components/workflow/types' type WorkflowVariableBlockComponentProps = { nodeKey: string variables: string[] workflowNodesMap: WorkflowNodesMap + environmentVariables?: Var[] + conversationVariables?: Var[] getVarType?: (payload: { nodeId: string, valueSelector: ValueSelector, @@ -49,6 +51,8 @@ const WorkflowVariableBlockComponent = ({ variables, workflowNodesMap = {}, getVarType, + environmentVariables, + conversationVariables, }: WorkflowVariableBlockComponentProps) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() @@ -68,6 +72,19 @@ const WorkflowVariableBlockComponent = ({ const isChatVar = isConversationVar(variables) const isException = isExceptionVariable(varName, node?.type) + let variableValid = true + if (isEnv) { + if (environmentVariables) + variableValid = environmentVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) + } + else if (isChatVar) { + if (conversationVariables) + variableValid = conversationVariables.some(v => v.variable === `${variables?.[0] ?? ''}.${variables?.[1] ?? ''}`) + } + else { + variableValid = !!node + } + const reactflow = useReactFlow() const store = useStoreApi() @@ -113,7 +130,7 @@ const WorkflowVariableBlockComponent = ({ className={cn( 'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] hover:border-state-accent-solid hover:bg-state-accent-hover', isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark', - !node && !isEnv && !isChatVar && '!border-state-destructive-solid !bg-state-destructive-hover', + !variableValid && '!border-state-destructive-solid !bg-state-destructive-hover', )} onClick={(e) => { e.stopPropagation() @@ -156,7 +173,7 @@ const WorkflowVariableBlockComponent = ({ isException && 'text-text-warning', )} title={varName}>{varName}</div> { - !node && !isEnv && !isChatVar && ( + !variableValid && ( <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' /> ) } @@ -164,7 +181,7 @@ const WorkflowVariableBlockComponent = ({ </div> ) - if (!node && !isEnv && !isChatVar) { + if (!variableValid) { return ( <Tooltip popupContent={t('workflow.errorMsg.invalidVariable')}> {Item} diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index dce636d92d..f828bdbc14 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -3,6 +3,7 @@ import { DecoratorNode } from 'lexical' import type { WorkflowVariableBlockType } from '../../types' import WorkflowVariableBlockComponent from './component' import type { GetVarType } from '../../types' +import type { Var } from '@/app/components/workflow/types' export type WorkflowNodesMap = WorkflowVariableBlockType['workflowNodesMap'] @@ -10,31 +11,37 @@ export type SerializedNode = SerializedLexicalNode & { variables: string[] workflowNodesMap: WorkflowNodesMap getVarType?: GetVarType + environmentVariables?: Var[] + conversationVariables?: Var[] } export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> { __variables: string[] __workflowNodesMap: WorkflowNodesMap __getVarType?: GetVarType + __environmentVariables?: Var[] + __conversationVariables?: Var[] static getType(): string { return 'workflow-variable-block' } static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { - return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key) + return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__getVarType, node.__key, node.__environmentVariables, node.__conversationVariables) } isInline(): boolean { return true } - constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey) { + constructor(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType: any, key?: NodeKey, environmentVariables?: Var[], conversationVariables?: Var[]) { super(key) this.__variables = variables this.__workflowNodesMap = workflowNodesMap this.__getVarType = getVarType + this.__environmentVariables = environmentVariables + this.__conversationVariables = conversationVariables } createDOM(): HTMLElement { @@ -54,12 +61,14 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> variables={this.__variables} workflowNodesMap={this.__workflowNodesMap} getVarType={this.__getVarType!} + environmentVariables={this.__environmentVariables} + conversationVariables={this.__conversationVariables} /> ) } static importJSON(serializedNode: SerializedNode): WorkflowVariableBlockNode { - const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType) + const node = $createWorkflowVariableBlockNode(serializedNode.variables, serializedNode.workflowNodesMap, serializedNode.getVarType, serializedNode.environmentVariables, serializedNode.conversationVariables) return node } @@ -71,6 +80,8 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> variables: this.getVariables(), workflowNodesMap: this.getWorkflowNodesMap(), getVarType: this.getVarType(), + environmentVariables: this.getEnvironmentVariables(), + conversationVariables: this.getConversationVariables(), } } @@ -89,12 +100,22 @@ export class WorkflowVariableBlockNode extends DecoratorNode<React.JSX.Element> return self.__getVarType } + getEnvironmentVariables(): any { + const self = this.getLatest() + return self.__environmentVariables + } + + getConversationVariables(): any { + const self = this.getLatest() + return self.__conversationVariables + } + getTextContent(): string { return `{{#${this.getVariables().join('.')}#}}` } } -export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType): WorkflowVariableBlockNode { - return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType) +export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap, getVarType?: GetVarType, environmentVariables?: Var[], conversationVariables?: Var[]): WorkflowVariableBlockNode { + return new WorkflowVariableBlockNode(variables, workflowNodesMap, getVarType, undefined, environmentVariables, conversationVariables) } export function $isWorkflowVariableBlockNode( diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx index 288008bbcc..4d0c80f10f 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/workflow-variable-block-replacement-block.tsx @@ -18,6 +18,7 @@ const WorkflowVariableBlockReplacementBlock = ({ workflowNodesMap, getVarType, onInsert, + variables, }: WorkflowVariableBlockType) => { const [editor] = useLexicalComposerContext() @@ -31,8 +32,8 @@ const WorkflowVariableBlockReplacementBlock = ({ onInsert() const nodePathString = textNode.getTextContent().slice(3, -3) - return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType)) - }, [onInsert, workflowNodesMap, getVarType]) + return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap, getVarType, variables?.find(o => o.nodeId === 'env')?.vars || [], variables?.find(o => o.nodeId === 'conversation')?.vars || [])) + }, [onInsert, workflowNodesMap, getVarType, variables]) const getMatch = useCallback((text: string) => { const matchArr = REGEX.exec(text) diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index fa8730f698..77d229672f 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -1,10 +1,10 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import Badge from '../badge/index' -import { RiCheckLine } from '@remixicon/react' +import { RiCheckLine, RiLoader4Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' import classNames from '@/utils/classnames' import { @@ -51,6 +51,8 @@ export type ISelectProps = { item: Item selected: boolean }) => React.ReactNode + isLoading?: boolean + onOpenChange?: (open: boolean) => void } const Select: FC<ISelectProps> = ({ className, @@ -114,7 +116,7 @@ const Select: FC<ISelectProps> = ({ if (!disabled) setOpen(!open) } - } className={classNames(`flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover group-hover:bg-state-base-hover`, optionClassName)}> + } className={classNames(`flex h-9 w-full items-center rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6`, optionClassName)}> <div className='w-0 grow truncate text-left' title={selectedItem?.name}>{selectedItem?.name}</div> </ComboboxButton>} <ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" onClick={ @@ -135,7 +137,7 @@ const Select: FC<ISelectProps> = ({ value={item} className={({ active }: { active: boolean }) => classNames( - 'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', + 'relative cursor-default select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover', active ? 'bg-state-base-hover' : '', optionClassName, ) @@ -178,17 +180,20 @@ const SimpleSelect: FC<ISelectProps> = ({ defaultValue = 1, disabled = false, onSelect, + onOpenChange, placeholder, optionWrapClassName, optionClassName, hideChecked, notClearable, renderOption, + isLoading = false, }) => { const { t } = useTranslation() const localPlaceholder = placeholder || t('common.placeholder.select') const [selectedItem, setSelectedItem] = useState<Item | null>(null) + useEffect(() => { let defaultSelect = null const existed = items.find((item: Item) => item.value === defaultValue) @@ -199,8 +204,10 @@ const SimpleSelect: FC<ISelectProps> = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultValue]) + const listboxRef = useRef<HTMLDivElement>(null) + return ( - <Listbox + <Listbox ref={listboxRef} value={selectedItem} onChange={(value: Item) => { if (!disabled) { @@ -212,10 +219,17 @@ const SimpleSelect: FC<ISelectProps> = ({ <div className={classNames('group/simple-select relative h-9', wrapperClassName)}> {renderTrigger && <ListboxButton className='w-full'>{renderTrigger(selectedItem)}</ListboxButton>} {!renderTrigger && ( - <ListboxButton className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> - <span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> + <ListboxButton onClick={() => { + // get data-open, use setTimeout to ensure the attribute is set + setTimeout(() => { + if (listboxRef.current) + onOpenChange?.(listboxRef.current.getAttribute('data-open') !== null) + }) + }} className={classNames(`flex h-full w-full items-center rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 focus-visible:bg-state-base-hover-alt focus-visible:outline-none group-hover/simple-select:bg-state-base-hover-alt sm:text-sm sm:leading-6 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}> + <span className={classNames('system-sm-regular block truncate text-left text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span> <span className="absolute inset-y-0 right-0 flex items-center pr-2"> - {(selectedItem && !notClearable) + {isLoading ? <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' /> + : (selectedItem && !notClearable) ? ( <XMarkIcon onClick={(e) => { @@ -237,14 +251,14 @@ const SimpleSelect: FC<ISelectProps> = ({ </ListboxButton> )} - {!disabled && ( - <ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-xl bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}> + {(!disabled) && ( + <ListboxOptions className={classNames('absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-1 py-1 text-base shadow-lg backdrop-blur-sm focus:outline-none sm:text-sm', optionWrapClassName)}> {items.map((item: Item) => ( <ListboxOption key={item.value} className={ classNames( - 'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary', + 'relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover', optionClassName, ) } @@ -324,7 +338,7 @@ const PortalSelect: FC<PortalSelectProps> = ({ : ( <div className={classNames(` - group flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-components-input-bg-normal hover:bg-state-base-hover-alt text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'} + group flex h-9 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2.5 text-sm hover:bg-state-base-hover-alt ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'} `, triggerClassName, triggerClassNameFn?.(open))} title={selectedItem?.name} > @@ -344,7 +358,7 @@ const PortalSelect: FC<PortalSelectProps> = ({ </PortalToFollowElemTrigger> <PortalToFollowElemContent className={`z-20 ${popupClassName}`}> <div - className={classNames('px-1 py-1 max-h-60 overflow-auto rounded-md text-base shadow-lg border-components-panel-border bg-components-panel-bg border-[0.5px] focus:outline-none sm:text-sm', popupInnerClassName)} + className={classNames('max-h-60 overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm', popupInnerClassName)} > {items.map((item: Item) => ( <div diff --git a/web/app/components/base/tab-header/index.tsx b/web/app/components/base/tab-header/index.tsx index 36dfa8c68f..846277e5db 100644 --- a/web/app/components/base/tab-header/index.tsx +++ b/web/app/components/base/tab-header/index.tsx @@ -9,30 +9,34 @@ type Item = { isRight?: boolean icon?: React.ReactNode extra?: React.ReactNode + disabled?: boolean } export type ITabHeaderProps = { items: Item[] value: string + itemClassName?: string onChange: (value: string) => void } const TabHeader: FC<ITabHeaderProps> = ({ items, value, + itemClassName, onChange, }) => { - const renderItem = ({ id, name, icon, extra }: Item) => ( + const renderItem = ({ id, name, icon, extra, disabled }: Item) => ( <div key={id} className={cn( 'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5', id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary', + disabled && 'cursor-not-allowed opacity-30', )} - onClick={() => onChange(id)} + onClick={() => !disabled && onChange(id)} > {icon || ''} - <div className='ml-2'>{name}</div> + <div className={cn('ml-2', itemClassName)}>{name}</div> {extra || ''} </div> ) diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index 4824b6f62d..eeed13c567 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -1,6 +1,5 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import type { ChangeEvent, FC, KeyboardEvent } from 'react' -import { } from 'use-context-selector' import { useTranslation } from 'react-i18next' import AutosizeInput from 'react-18-input-autosize' import { RiAddLine, RiCloseLine } from '@remixicon/react' @@ -40,6 +39,29 @@ const TagInput: FC<TagInputProps> = ({ onChange(copyItems) } + const handleNewTag = useCallback((value: string) => { + const valueTrimmed = value.trim() + if (!valueTrimmed) { + notify({ type: 'error', message: t('datasetDocuments.segment.keywordEmpty') }) + return + } + + if ((items.find(item => item === valueTrimmed))) { + notify({ type: 'error', message: t('datasetDocuments.segment.keywordDuplicate') }) + return + } + + if (valueTrimmed.length > 20) { + notify({ type: 'error', message: t('datasetDocuments.segment.keywordError') }) + return + } + + onChange([...items, valueTrimmed]) + setTimeout(() => { + setValue('') + }) + }, [items, onChange, notify, t]) + const handleKeyDown = (e: KeyboardEvent) => { if (isSpecialMode && e.key === 'Enter') setValue(`${value}↵`) @@ -48,24 +70,12 @@ const TagInput: FC<TagInputProps> = ({ if (isSpecialMode) e.preventDefault() - const valueTrimmed = value.trim() - if (!valueTrimmed || (items.find(item => item === valueTrimmed))) - return - - if (valueTrimmed.length > 20) { - notify({ type: 'error', message: t('datasetDocuments.segment.keywordError') }) - return - } - - onChange([...items, valueTrimmed]) - setTimeout(() => { - setValue('') - }) + handleNewTag(value) } } const handleBlur = () => { - setValue('') + handleNewTag(value) setFocused(false) } diff --git a/web/app/components/base/tag-management/tag-item-editor.tsx b/web/app/components/base/tag-management/tag-item-editor.tsx index 3264979955..0fdfc5079a 100644 --- a/web/app/components/base/tag-management/tag-item-editor.tsx +++ b/web/app/components/base/tag-management/tag-item-editor.tsx @@ -12,6 +12,7 @@ import Confirm from '@/app/components/base/confirm' import cn from '@/utils/classnames' import type { Tag } from '@/app/components/base/tag-management/constant' import { ToastContext } from '@/app/components/base/toast' +import Tooltip from '@/app/components/base/tooltip' import { deleteTag, updateTag, @@ -109,7 +110,14 @@ const TagItemEditor: FC<TagItemEditorProps> = ({ <div className='text-sm leading-5 text-text-secondary'> {tag.name} </div> - <div className='leading-4.5 shrink-0 px-1 text-sm font-medium text-text-tertiary'>{tag.binding_count}</div> + <Tooltip + popupContent={ + <div>{t('workflow.common.tagBound')}</div> + } + needsDelay + > + <div className='leading-4.5 shrink-0 px-1 text-sm font-medium text-text-tertiary'>{tag.binding_count}</div> + </Tooltip> <div className='group/edit shrink-0 cursor-pointer rounded-md p-1 hover:bg-state-base-hover' onClick={() => setIsEditing(true)}> <RiEditLine className='h-3 w-3 text-text-tertiary group-hover/edit:text-text-secondary' /> </div> diff --git a/web/app/components/base/tooltip/index.spec.tsx b/web/app/components/base/tooltip/index.spec.tsx index 1b9b7a0eee..38cb107197 100644 --- a/web/app/components/base/tooltip/index.spec.tsx +++ b/web/app/components/base/tooltip/index.spec.tsx @@ -50,7 +50,7 @@ describe('Tooltip', () => { test('should close on mouse leave when triggerMethod is hover', () => { const triggerClassName = 'custom-trigger' - const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />) + const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} needsDelay={false} />) const trigger = container.querySelector(`.${triggerClassName}`) act(() => { fireEvent.mouseEnter(trigger!) diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx index 53f36be5fb..92a1afe691 100644 --- a/web/app/components/base/tooltip/index.tsx +++ b/web/app/components/base/tooltip/index.tsx @@ -33,7 +33,7 @@ const Tooltip: FC<TooltipProps> = ({ noDecoration, offset, asChild = true, - needsDelay = false, + needsDelay = true, }) => { const [open, setOpen] = useState(false) const [isHoverPopup, { @@ -68,7 +68,7 @@ const Tooltip: FC<TooltipProps> = ({ setTimeout(() => { if (!isHoverPopupRef.current && !isHoverTriggerRef.current) setOpen(false) - }, 500) + }, 300) } else { setOpen(false) @@ -101,7 +101,7 @@ const Tooltip: FC<TooltipProps> = ({ > {popupContent && (<div className={cn( - !noDecoration && 'system-xs-regular relative break-words rounded-md bg-components-panel-bg px-3 py-2 text-text-tertiary shadow-lg', + !noDecoration && 'system-xs-regular relative max-w-[300px] break-words rounded-md bg-components-panel-bg px-3 py-2 text-left text-text-tertiary shadow-lg', popupClassName, )} onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()} diff --git a/web/app/components/datasets/common/document-picker/document-list.tsx b/web/app/components/datasets/common/document-picker/document-list.tsx index 5ac98b97fe..6b51973ccc 100644 --- a/web/app/components/datasets/common/document-picker/document-list.tsx +++ b/web/app/components/datasets/common/document-picker/document-list.tsx @@ -21,7 +21,7 @@ const DocumentList: FC<Props> = ({ }, [onChange]) return ( - <div className={cn(className)}> + <div className={cn('max-h-[calc(100vh-120px)] overflow-auto', className)}> {list.map((item) => { const { id, name, extension } = item return ( diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index b1e4087226..a1ff2f5d87 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -122,7 +122,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} /> return ( - <div className='flex flex-col bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}> + <div className='flex flex-col overflow-hidden bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}> <TopBar activeIndex={step - 1} datasetId={datasetId} /> <div style={{ height: 'calc(100% - 52px)' }}> {step === 1 && <StepOne diff --git a/web/app/components/datasets/create/notion-page-preview/index.module.css b/web/app/components/datasets/create/notion-page-preview/index.module.css index b3bc589480..4d0eb1b672 100644 --- a/web/app/components/datasets/create/notion-page-preview/index.module.css +++ b/web/app/components/datasets/create/notion-page-preview/index.module.css @@ -1,6 +1,5 @@ .filePreview { @apply flex flex-col border-l border-components-panel-border shrink-0 bg-background-default-lighter; - width: 528px; } .previewHeader { @@ -28,6 +27,7 @@ background: #f9fafb center no-repeat url(../assets/Loading.svg); background-size: contain; } + .fileContent { white-space: pre-line; } diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 11664c7ada..f4aae9d1f1 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react' import FilePreview from '../file-preview' @@ -95,24 +95,29 @@ const StepOne = ({ const modalShowHandle = () => setShowModal(true) const modalCloseHandle = () => setShowModal(false) - const updateCurrentFile = (file: File) => { + const updateCurrentFile = useCallback((file: File) => { setCurrentFile(file) - } - const hideFilePreview = () => { + }, []) + + const hideFilePreview = useCallback(() => { setCurrentFile(undefined) - } + }, []) - const updateCurrentPage = (page: NotionPage) => { + const updateCurrentPage = useCallback((page: NotionPage) => { setCurrentNotionPage(page) - } + }, []) - const hideNotionPagePreview = () => { + const hideNotionPagePreview = useCallback(() => { setCurrentNotionPage(undefined) - } + }, []) + + const updateWebsite = useCallback((website: CrawlResultItem) => { + setCurrentWebsite(website) + }, []) - const hideWebsitePreview = () => { + const hideWebsitePreview = useCallback(() => { setCurrentWebsite(undefined) - } + }, []) const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type) const isInCreatePage = shouldShowDataSourceTypeList @@ -139,7 +144,7 @@ const StepOne = ({ <div className={classNames(s.form)}> { shouldShowDataSourceTypeList && ( - <div className={classNames(s.stepHeader, 'text-text-secondary system-md-semibold')}> + <div className={classNames(s.stepHeader, 'system-md-semibold text-text-secondary')}> {t('datasetCreation.steps.one')} </div> ) @@ -158,8 +163,8 @@ const StepOne = ({ if (dataSourceTypeDisable) return changeType(DataSourceType.FILE) - hideFilePreview() hideNotionPagePreview() + hideWebsitePreview() }} > <span className={cn(s.datasetIcon)} /> @@ -182,7 +187,7 @@ const StepOne = ({ return changeType(DataSourceType.NOTION) hideFilePreview() - hideNotionPagePreview() + hideWebsitePreview() }} > <span className={cn(s.datasetIcon, s.notion)} /> @@ -201,7 +206,13 @@ const StepOne = ({ dataSourceType === DataSourceType.WEB && s.active, dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled, )} - onClick={() => changeType(DataSourceType.WEB)} + onClick={() => { + if (dataSourceTypeDisable) + return + changeType(DataSourceType.WEB) + hideFilePreview() + hideNotionPagePreview() + }} > <span className={cn(s.datasetIcon, s.web)} /> <span @@ -276,7 +287,7 @@ const StepOne = ({ <> <div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}> <Website - onPreview={setCurrentWebsite} + onPreview={updateWebsite} checkedCrawlResult={websitePages} onCheckedCrawlResultChange={updateWebsitePages} onCrawlProviderChange={onWebsiteCrawlProviderChange} diff --git a/web/app/components/datasets/create/step-three/index.tsx b/web/app/components/datasets/create/step-three/index.tsx index cdaa003a23..ea5977a02a 100644 --- a/web/app/components/datasets/create/step-three/index.tsx +++ b/web/app/components/datasets/create/step-three/index.tsx @@ -52,8 +52,8 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie datasetId={datasetId || creationCache?.dataset?.id || ''} batchId={creationCache?.batch || ''} documents={creationCache?.documents as FullDocumentDetail[]} - indexingType={indexingType || creationCache?.dataset?.indexing_technique} - retrievalMethod={retrievalMethod || creationCache?.dataset?.retrieval_model?.search_method} + indexingType={creationCache?.dataset?.indexing_technique || indexingType} + retrievalMethod={creationCache?.dataset?.retrieval_model_dict?.search_method || retrievalMethod} /> </div> </div> diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 52c5195bbf..ebd8552a99 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -63,6 +63,7 @@ import CustomDialog from '@/app/components/base/dialog' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const TextLabel: FC<PropsWithChildren> = (props) => { return <label className='system-sm-semibold text-text-secondary'>{props.children}</label> @@ -146,6 +147,7 @@ const StepTwo = ({ updateRetrievalMethodCache, }: StepTwoProps) => { const { t } = useTranslation() + const docLink = useDocLink() const { locale } = useContext(I18n) const media = useBreakpoints() const isMobile = media === MediaType.mobile @@ -159,7 +161,9 @@ const StepTwo = ({ const isInCreatePage = !datasetId || (datasetId && !currentDataset?.data_source_type) const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : currentDataset?.data_source_type - const [segmentationType, setSegmentationType] = useState<ProcessMode>(ProcessMode.general) + const [segmentationType, setSegmentationType] = useState<ProcessMode>( + currentDataset?.doc_form === ChunkingMode.parentChild ? ProcessMode.parentChild : ProcessMode.general, + ) const [segmentIdentifier, doSetSegmentIdentifier] = useState(DEFAULT_SEGMENT_IDENTIFIER) const setSegmentIdentifier = useCallback((value: string, canEmpty?: boolean) => { doSetSegmentIdentifier(value ? escape(value) : (canEmpty ? '' : DEFAULT_SEGMENT_IDENTIFIER)) @@ -205,7 +209,14 @@ const StepTwo = ({ } if (value === ChunkingMode.parentChild && indexType === IndexingType.ECONOMICAL) setIndexType(IndexingType.QUALIFIED) + setDocForm(value) + + if (value === ChunkingMode.parentChild) + setSegmentationType(ProcessMode.parentChild) + else + setSegmentationType(ProcessMode.general) + // eslint-disable-next-line ts/no-use-before-define currentEstimateMutation.reset() } @@ -496,11 +507,27 @@ const StepTwo = ({ const separator = rules.segmentation.separator const max = rules.segmentation.max_tokens const overlap = rules.segmentation.chunk_overlap + const isHierarchicalDocument = documentDetail.doc_form === ChunkingMode.parentChild + || (rules.parent_mode && rules.subchunk_segmentation) setSegmentIdentifier(separator) setMaxChunkLength(max) setOverlap(overlap!) setRules(rules.pre_processing_rules) setDefaultConfig(rules) + + if (isHierarchicalDocument) { + setParentChildConfig({ + chunkForContext: rules.parent_mode || 'paragraph', + parent: { + delimiter: escape(rules.segmentation.separator), + maxLength: rules.segmentation.max_tokens, + }, + child: { + delimiter: escape(rules.subchunk_segmentation.separator), + maxLength: rules.subchunk_segmentation.max_tokens, + }, + }) + } } } @@ -550,6 +577,7 @@ const StepTwo = ({ onSuccess(data) { updateIndexingTypeCache && updateIndexingTypeCache(indexType as string) updateResultCache && updateResultCache(data) + updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string) }, }) } @@ -962,7 +990,9 @@ const StepTwo = ({ <div className={'mb-1'}> <div className='system-md-semibold mb-0.5 text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='body-xs-regular text-text-tertiary'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a target='_blank' rel='noopener noreferrer' + href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents')} + className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> {t('datasetSettings.form.retrievalSetting.longDescription')} </div> </div> @@ -1126,7 +1156,7 @@ const StepTwo = ({ const indexForLabel = index + 1 return ( <PreviewSlice - key={child} + key={`C-${indexForLabel}-${child}`} label={`C-${indexForLabel}`} text={child} tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`} diff --git a/web/app/components/datasets/create/website/base/url-input.tsx b/web/app/components/datasets/create/website/base/url-input.tsx index b7dc9bfca5..ab965bebc3 100644 --- a/web/app/components/datasets/create/website/base/url-input.tsx +++ b/web/app/components/datasets/create/website/base/url-input.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from './input' import Button from '@/app/components/base/button' +import { useDocLink } from '@/context/i18n' const I18N_PREFIX = 'datasetCreation.stepOne.website' @@ -17,6 +18,7 @@ const UrlInput: FC<Props> = ({ onRun, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [url, setUrl] = useState('') const handleUrlChange = useCallback((url: string | number) => { setUrl(url as string) @@ -32,7 +34,7 @@ const UrlInput: FC<Props> = ({ <Input value={url} onChange={handleUrlChange} - placeholder='https://docs.dify.ai' + placeholder={docLink()} /> <Button variant='primary' diff --git a/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx b/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx index e6b0475874..ee300cb567 100644 --- a/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx +++ b/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from './input' import Button from '@/app/components/base/button' +import { useDocLink } from '@/context/i18n' const I18N_PREFIX = 'datasetCreation.stepOne.website' @@ -17,6 +18,7 @@ const UrlInput: FC<Props> = ({ onRun, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [url, setUrl] = useState('') const handleUrlChange = useCallback((url: string | number) => { setUrl(url as string) @@ -32,7 +34,7 @@ const UrlInput: FC<Props> = ({ <Input value={url} onChange={handleUrlChange} - placeholder='https://docs.dify.ai' + placeholder={docLink()} /> <Button variant='primary' diff --git a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx index d3575c18ed..f3f0aef6ac 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx @@ -1,4 +1,4 @@ -import React, { type FC, useMemo, useState } from 'react' +import React, { type FC, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiCloseLine, @@ -16,8 +16,10 @@ import { useSegmentListContext } from './index' import { ChunkingMode, type SegmentDetailModel } from '@/models/datasets' import { useEventEmitterContextContext } from '@/context/event-emitter' import { formatNumber } from '@/utils/format' -import classNames from '@/utils/classnames' +import cn from '@/utils/classnames' import Divider from '@/app/components/base/divider' +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import { IndexingType } from '../../../create/step-two' type ISegmentDetailProps = { segInfo?: Partial<SegmentDetailModel> & { id: string } @@ -48,6 +50,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({ const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen) const mode = useDocumentContext(s => s.mode) const parentMode = useDocumentContext(s => s.parentMode) + const indexingTechnique = useDatasetDetailContextWithSelector(s => s.dataset?.indexing_technique) eventEmitter?.useSubscription((v) => { if (v === 'update-segment') @@ -56,56 +59,41 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({ setLoading(false) }) - const handleCancel = () => { + const handleCancel = useCallback(() => { onCancel() - } + }, [onCancel]) - const handleSave = () => { + const handleSave = useCallback(() => { onUpdate(segInfo?.id || '', question, answer, keywords) - } + }, [onUpdate, segInfo?.id, question, answer, keywords]) - const handleRegeneration = () => { + const handleRegeneration = useCallback(() => { setShowRegenerationModal(true) - } + }, []) - const onCancelRegeneration = () => { + const onCancelRegeneration = useCallback(() => { setShowRegenerationModal(false) - } + }, []) - const onConfirmRegeneration = () => { + const onConfirmRegeneration = useCallback(() => { onUpdate(segInfo?.id || '', question, answer, keywords, true) - } - - const isParentChildMode = useMemo(() => { - return mode === 'hierarchical' - }, [mode]) - - const isFullDocMode = useMemo(() => { - return mode === 'hierarchical' && parentMode === 'full-doc' - }, [mode, parentMode]) - - const titleText = useMemo(() => { - return isEditMode ? t('datasetDocuments.segment.editChunk') : t('datasetDocuments.segment.chunkDetail') - }, [isEditMode, t]) - - const isQAModel = useMemo(() => { - return docForm === ChunkingMode.qa - }, [docForm]) + }, [onUpdate, segInfo?.id, question, answer, keywords]) const wordCountText = useMemo(() => { - const contentLength = isQAModel ? (question.length + answer.length) : question.length + const contentLength = docForm === ChunkingMode.qa ? (question.length + answer.length) : question.length const total = formatNumber(isEditMode ? contentLength : segInfo!.word_count as number) const count = isEditMode ? contentLength : segInfo!.word_count as number return `${total} ${t('datasetDocuments.segment.characters', { count })}` - }, [isEditMode, question.length, answer.length, isQAModel, segInfo, t]) + }, [isEditMode, question.length, answer.length, docForm, segInfo, t]) - const labelPrefix = useMemo(() => { - return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') - }, [isParentChildMode, t]) + const isFullDocMode = mode === 'hierarchical' && parentMode === 'full-doc' + const titleText = isEditMode ? t('datasetDocuments.segment.editChunk') : t('datasetDocuments.segment.chunkDetail') + const labelPrefix = mode === 'hierarchical' ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') + const isECOIndexing = indexingTechnique === IndexingType.ECONOMICAL return ( <div className={'flex h-full flex-col'}> - <div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}> + <div className={cn('flex items-center justify-between', fullScreen ? 'border border-divider-subtle py-3 pl-6 pr-4' : 'pl-4 pr-3 pt-3')}> <div className='flex flex-col'> <div className='system-xl-semibold text-text-primary'>{titleText}</div> <div className='flex items-center gap-x-2'> @@ -134,12 +122,12 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({ </div> </div> </div> - <div className={classNames( + <div className={cn( 'flex grow', - fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4', - !isEditMode && 'pb-0 overflow-hidden', + fullScreen ? 'w-full flex-row justify-center gap-x-8 px-6 pt-6' : 'flex-col gap-y-1 px-4 py-3', + !isEditMode && 'overflow-hidden pb-0', )}> - <div className={classNames(isEditMode ? 'break-all whitespace-pre-line overflow-hidden' : 'overflow-y-auto', fullScreen ? 'w-1/2' : 'grow')}> + <div className={cn(isEditMode ? 'overflow-hidden whitespace-pre-line break-all' : 'overflow-y-auto', fullScreen ? 'w-1/2' : 'grow')}> <ChunkContent docForm={docForm} question={question} @@ -149,7 +137,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({ isEditMode={isEditMode} /> </div> - {mode === 'custom' && <Keywords + {isECOIndexing && <Keywords className={fullScreen ? 'w-1/5' : ''} actionType={isEditMode ? 'edit' : 'view'} segInfo={segInfo} diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 80f0f1dbb9..9c8aac6dca 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useMemo, useRef, useState } from 'react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -12,7 +12,6 @@ import Keywords from './completed/common/keywords' import ChunkContent from './completed/common/chunk-content' import AddAnother from './completed/common/add-another' import Dot from './completed/common/dot' -import { useDocumentContext } from './index' import { useStore as useAppStore } from '@/app/components/app/store' import { ToastContext } from '@/app/components/base/toast' import { ChunkingMode, type SegmentUpdater } from '@/models/datasets' @@ -20,6 +19,8 @@ import classNames from '@/utils/classnames' import { formatNumber } from '@/utils/format' import Divider from '@/app/components/base/divider' import { useAddSegment } from '@/service/knowledge/use-segment' +import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import { IndexingType } from '../../create/step-two' type NewSegmentModalProps = { onCancel: () => void @@ -44,39 +45,37 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({ const [addAnother, setAddAnother] = useState(true) const fullScreen = useSegmentListContext(s => s.fullScreen) const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen) - const mode = useDocumentContext(s => s.mode) + const indexingTechnique = useDatasetDetailContextWithSelector(s => s.dataset?.indexing_technique) const { appSidebarExpand } = useAppStore(useShallow(state => ({ appSidebarExpand: state.appSidebarExpand, }))) const refreshTimer = useRef<any>(null) - const CustomButton = <> - <Divider type='vertical' className='mx-1 h-3 bg-divider-regular' /> - <button - type='button' - className='system-xs-semibold text-text-accent' - onClick={() => { - clearTimeout(refreshTimer.current) - viewNewlyAddedChunk() - }}> - {t('common.operation.view')} - </button> - </> + const CustomButton = useMemo(() => ( + <> + <Divider type='vertical' className='mx-1 h-3 bg-divider-regular' /> + <button + type='button' + className='system-xs-semibold text-text-accent' + onClick={() => { + clearTimeout(refreshTimer.current) + viewNewlyAddedChunk() + }}> + {t('common.operation.view')} + </button> + </> + ), [viewNewlyAddedChunk, t]) - const isQAModel = useMemo(() => { - return docForm === ChunkingMode.qa - }, [docForm]) - - const handleCancel = (actionType: 'esc' | 'add' = 'esc') => { + const handleCancel = useCallback((actionType: 'esc' | 'add' = 'esc') => { if (actionType === 'esc' || !addAnother) onCancel() - } + }, [onCancel, addAnother]) const { mutateAsync: addSegment } = useAddSegment() - const handleSave = async () => { + const handleSave = useCallback(async () => { const params: SegmentUpdater = { content: '' } - if (isQAModel) { + if (docForm === ChunkingMode.qa) { if (!question.trim()) { return notify({ type: 'error', @@ -129,21 +128,27 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({ setLoading(false) }, }) - } + }, [docForm, keywords, addSegment, datasetId, documentId, question, answer, notify, t, appSidebarExpand, CustomButton, handleCancel, onSave]) const wordCountText = useMemo(() => { - const count = isQAModel ? (question.length + answer.length) : question.length + const count = docForm === ChunkingMode.qa ? (question.length + answer.length) : question.length return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}` - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [question.length, answer.length, isQAModel]) + }, [question.length, answer.length, docForm, t]) + + const isECOIndexing = indexingTechnique === IndexingType.ECONOMICAL return ( <div className={'flex h-full flex-col'}> - <div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}> + <div + className={classNames( + 'flex items-center justify-between', + fullScreen ? 'border border-divider-subtle py-3 pl-6 pr-4' : 'pl-4 pr-3 pt-3', + )} + > <div className='flex flex-col'> - <div className='system-xl-semibold text-text-primary'>{ - t('datasetDocuments.segment.addChunk') - }</div> + <div className='system-xl-semibold text-text-primary'> + {t('datasetDocuments.segment.addChunk')} + </div> <div className='flex items-center gap-x-2'> <SegmentIndexTag label={t('datasetDocuments.segment.newChunk')!} /> <Dot /> @@ -171,8 +176,8 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({ </div> </div> </div> - <div className={classNames('flex grow', fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4')}> - <div className={classNames('break-all overflow-hidden whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}> + <div className={classNames('flex grow', fullScreen ? 'w-full flex-row justify-center gap-x-8 px-6 pt-6' : 'flex-col gap-y-1 px-4 py-3')}> + <div className={classNames('overflow-hidden whitespace-pre-line break-all', fullScreen ? 'w-1/2' : 'grow')}> <ChunkContent docForm={docForm} question={question} @@ -182,7 +187,7 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({ isEditMode={true} /> </div> - {mode === 'custom' && <Keywords + {isECOIndexing && <Keywords className={fullScreen ? 'w-1/5' : ''} actionType='add' keywords={keywords} diff --git a/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts new file mode 100644 index 0000000000..4531b7e658 --- /dev/null +++ b/web/app/components/datasets/documents/hooks/use-document-list-query-state.ts @@ -0,0 +1,83 @@ +import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useCallback, useMemo } from 'react' + +export type DocumentListQuery = { + page: number + limit: number + keyword: string +} + +const DEFAULT_QUERY: DocumentListQuery = { + page: 1, + limit: 10, + keyword: '', +} + +// Parse the query parameters from the URL search string. +function parseParams(params: ReadonlyURLSearchParams): DocumentListQuery { + const page = Number.parseInt(params.get('page') || '1', 10) + const limit = Number.parseInt(params.get('limit') || '10', 10) + const keyword = params.get('keyword') || '' + + return { + page: page > 0 ? page : 1, + limit: (limit > 0 && limit <= 100) ? limit : 10, + keyword: keyword ? decodeURIComponent(keyword) : '', + } +} + +// Update the URL search string with the given query parameters. +function updateSearchParams(query: DocumentListQuery, searchParams: URLSearchParams) { + const { page, limit, keyword } = query || {} + + const hasNonDefaultParams = (page && page > 1) || (limit && limit !== 10) || (keyword && keyword.trim()) + + if (hasNonDefaultParams) { + searchParams.set('page', (page || 1).toString()) + searchParams.set('limit', (limit || 10).toString()) + } + else { + searchParams.delete('page') + searchParams.delete('limit') + } + + if (keyword && keyword.trim()) + searchParams.set('keyword', encodeURIComponent(keyword)) + else + searchParams.delete('keyword') +} + +function useDocumentListQueryState() { + const searchParams = useSearchParams() + const query = useMemo(() => parseParams(searchParams), [searchParams]) + + const router = useRouter() + const pathname = usePathname() + + // Helper function to update specific query parameters + const updateQuery = useCallback((updates: Partial<DocumentListQuery>) => { + const newQuery = { ...query, ...updates } + const params = new URLSearchParams() + updateSearchParams(newQuery, params) + const search = params.toString() + const queryString = search ? `?${search}` : '' + router.push(`${pathname}${queryString}`, { scroll: false }) + }, [query, router, pathname]) + + // Helper function to reset query to defaults + const resetQuery = useCallback(() => { + const params = new URLSearchParams() + updateSearchParams(DEFAULT_QUERY, params) + const search = params.toString() + const queryString = search ? `?${search}` : '' + router.push(`${pathname}${queryString}`, { scroll: false }) + }, [router, pathname]) + + return useMemo(() => ({ + query, + updateQuery, + resetQuery, + }), [query, updateQuery, resetQuery]) +} + +export default useDocumentListQueryState diff --git a/web/app/components/datasets/documents/index.tsx b/web/app/components/datasets/documents/index.tsx index 2aecb587d0..676581a50f 100644 --- a/web/app/components/datasets/documents/index.tsx +++ b/web/app/components/datasets/documents/index.tsx @@ -26,11 +26,12 @@ import cn from '@/utils/classnames' import { useDocumentList, useInvalidDocumentDetailKey, useInvalidDocumentList } from '@/service/knowledge/use-document' import { useInvalid } from '@/service/use-base' import { useChildSegmentListKey, useSegmentListKey } from '@/service/knowledge/use-segment' +import useDocumentListQueryState from './hooks/use-document-list-query-state' import useEditDocumentMetadata from '../metadata/hooks/use-edit-dataset-metadata' import DatasetMetadataDrawer from '../metadata/metadata-dataset/dataset-metadata-drawer' import StatusWithAction from '../common/document-status-with-action/status-with-action' -import { LanguagesSupported } from '@/i18n/language' -import { getLocaleOnClient } from '@/i18n' +import { useDocLink } from '@/context/i18n' +import { useFetchDefaultProcessRule } from '@/service/knowledge/use-create-dataset' const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => { return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}> @@ -82,16 +83,20 @@ type IDocumentsProps = { } export const fetcher = (url: string) => get(url, {}, {}) -const DEFAULT_LIMIT = 10 const Documents: FC<IDocumentsProps> = ({ datasetId }) => { const { t } = useTranslation() + const docLink = useDocLink() const { plan } = useProviderContext() const isFreePlan = plan.type === 'sandbox' const [inputValue, setInputValue] = useState<string>('') // the input value const [searchValue, setSearchValue] = useState<string>('') - const [currPage, setCurrPage] = React.useState<number>(0) - const [limit, setLimit] = useState<number>(DEFAULT_LIMIT) + + // Use the new hook for URL state management + const { query, updateQuery } = useDocumentListQueryState() + const [currPage, setCurrPage] = React.useState<number>(query.page - 1) // Convert to 0-based index + const [limit, setLimit] = useState<number>(query.limit) + const router = useRouter() const { dataset } = useDatasetDetailContext() const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false) @@ -100,9 +105,47 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { const isDataSourceWeb = dataset?.data_source_type === DataSourceType.WEB const isDataSourceFile = dataset?.data_source_type === DataSourceType.FILE const embeddingAvailable = !!dataset?.embedding_available - const locale = getLocaleOnClient() const debouncedSearchValue = useDebounce(searchValue, { wait: 500 }) + // Initialize search value from URL on mount + useEffect(() => { + if (query.keyword) { + setInputValue(query.keyword) + setSearchValue(query.keyword) + } + }, []) // Only run on mount + + // Sync local state with URL query changes + useEffect(() => { + setCurrPage(query.page - 1) + setLimit(query.limit) + if (query.keyword !== searchValue) { + setInputValue(query.keyword) + setSearchValue(query.keyword) + } + }, [query]) + + // Update URL when pagination changes + const handlePageChange = (newPage: number) => { + setCurrPage(newPage) + updateQuery({ page: newPage + 1 }) // Convert to 1-based index + } + + // Update URL when limit changes + const handleLimitChange = (newLimit: number) => { + setLimit(newLimit) + setCurrPage(0) // Reset to first page when limit changes + updateQuery({ limit: newLimit, page: 1 }) + } + + // Update URL when search changes + useEffect(() => { + if (debouncedSearchValue !== query.keyword) { + setCurrPage(0) // Reset to first page when search changes + updateQuery({ keyword: debouncedSearchValue, page: 1 }) + } + }, [debouncedSearchValue, query.keyword, updateQuery]) + const { data: documentsRes, isFetching: isListLoading } = useDocumentList({ datasetId, query: { @@ -179,6 +222,8 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { router.push(`/datasets/${datasetId}/documents/create`) } + const fetchDefaultProcessRuleMutation = useFetchDefaultProcessRule() + const handleSaveNotionPageSelected = async (selectedPages: NotionPage[]) => { const workspacesMap = groupBy(selectedPages, 'workspace_id') const workspaces = Object.keys(workspacesMap).map((workspaceId) => { @@ -187,6 +232,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { pages: workspacesMap[workspaceId], } }) + const { rules } = await fetchDefaultProcessRuleMutation.mutateAsync('/datasets/process-rule') const params = { data_source: { type: dataset?.data_source_type, @@ -210,7 +256,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { }, indexing_technique: dataset?.indexing_technique, process_rule: { - rules: {}, + rules, mode: ProcessMode.general, }, } as CreateDocumentReq @@ -262,11 +308,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { <a className='flex items-center text-text-accent' target='_blank' - href={ - locale === LanguagesSupported[1] - ? 'https://docs.dify.ai/zh-hans/guides/knowledge-base/integrate-knowledge-within-application' - : 'https://docs.dify.ai/en/guides/knowledge-base/integrate-knowledge-within-application' - } + href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')} > <span>{t('datasetDocuments.list.learnMore')}</span> <RiExternalLinkLine className='h-3 w-3' /> @@ -328,9 +370,9 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => { pagination={{ total, limit, - onLimitChange: setLimit, + onLimitChange: handleLimitChange, current: currPage, - onChange: setCurrPage, + onChange: handlePageChange, }} onManageMetadata={showEditMetadataModal} /> diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index cb349ee01c..2eb6a3ac1e 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -11,6 +11,8 @@ import { RiEqualizer2Line, RiLoopLeftLine, RiMoreFill, + RiPauseCircleLine, + RiPlayCircleLine, } from '@remixicon/react' import { useContext } from 'use-context-selector' import { useRouter } from 'next/navigation' @@ -42,7 +44,7 @@ import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from ' import type { Props as PaginationProps } from '@/app/components/base/pagination' import Pagination from '@/app/components/base/pagination' import Checkbox from '@/app/components/base/checkbox' -import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document' +import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentPause, useDocumentResume, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document' import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata' import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal' @@ -152,7 +154,6 @@ export const StatusItem: FC<{ <Tooltip popupContent={t('datasetDocuments.list.action.enableWarning')} popupClassName='text-text-secondary system-xs-medium' - needsDelay disabled={!archived} > <Switch @@ -168,7 +169,7 @@ export const StatusItem: FC<{ </div> } -type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' +type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' | 'pause' | 'resume' // operation action for list and detail export const OperationAction: FC<{ @@ -180,13 +181,14 @@ export const OperationAction: FC<{ id: string data_source_type: string doc_form: string + display_status?: string } datasetId: string onUpdate: (operationName?: string) => void scene?: 'list' | 'detail' className?: string }> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => { - const { id, enabled = false, archived = false, data_source_type } = detail || {} + const { id, enabled = false, archived = false, data_source_type, display_status } = detail || {} const [showModal, setShowModal] = useState(false) const [deleting, setDeleting] = useState(false) const { notify } = useContext(ToastContext) @@ -199,6 +201,8 @@ export const OperationAction: FC<{ const { mutateAsync: deleteDocument } = useDocumentDelete() const { mutateAsync: syncDocument } = useSyncDocument() const { mutateAsync: syncWebsite } = useSyncWebsite() + const { mutateAsync: pauseDocument } = useDocumentPause() + const { mutateAsync: resumeDocument } = useDocumentResume() const isListScene = scene === 'list' const onOperate = async (operationName: OperationName) => { @@ -222,6 +226,12 @@ export const OperationAction: FC<{ else opApi = syncWebsite break + case 'pause': + opApi = pauseDocument + break + case 'resume': + opApi = resumeDocument + break default: opApi = deleteDocument setDeleting(true) @@ -274,7 +284,6 @@ export const OperationAction: FC<{ ? <Tooltip popupContent={t('datasetDocuments.list.action.enableWarning')} popupClassName='!font-semibold' - needsDelay > <div> <Switch defaultValue={false} onChange={noop} disabled={true} size='md' /> @@ -323,6 +332,18 @@ export const OperationAction: FC<{ <Divider className='my-1' /> </> )} + {!archived && display_status?.toLowerCase() === 'indexing' && ( + <div className={s.actionItem} onClick={() => onOperate('pause')}> + <RiPauseCircleLine className='h-4 w-4 text-text-tertiary' /> + <span className={s.actionName}>{t('datasetDocuments.list.action.pause')}</span> + </div> + )} + {!archived && display_status?.toLowerCase() === 'paused' && ( + <div className={s.actionItem} onClick={() => onOperate('resume')}> + <RiPlayCircleLine className='h-4 w-4 text-text-tertiary' /> + <span className={s.actionName}>{t('datasetDocuments.list.action.resume')}</span> + </div> + )} {!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}> <RiArchive2Line className='h-4 w-4 text-text-tertiary' /> <span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span> @@ -575,7 +596,6 @@ const DocumentList: FC<IDocumentListProps> = ({ ) }} /> - {/* {doc.position} */} {index + 1} </div> </td> @@ -626,7 +646,7 @@ const DocumentList: FC<IDocumentListProps> = ({ <OperationAction embeddingAvailable={embeddingAvailable} datasetId={datasetId} - detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form'])} + detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form', 'display_status'])} onUpdate={onUpdate} /> </td> diff --git a/web/app/components/datasets/external-api/external-api-modal/Form.tsx b/web/app/components/datasets/external-api/external-api-modal/Form.tsx index 2746a32a5b..8884cb787f 100644 --- a/web/app/components/datasets/external-api/external-api-modal/Form.tsx +++ b/web/app/components/datasets/external-api/external-api-modal/Form.tsx @@ -5,6 +5,7 @@ import { RiBookOpenLine } from '@remixicon/react' import type { CreateExternalAPIReq, FormSchema } from '../declarations' import Input from '@/app/components/base/input' import cn from '@/utils/classnames' +import { useDocLink } from '@/context/i18n' type FormProps = { className?: string @@ -26,6 +27,7 @@ const Form: FC<FormProps> = React.memo(({ inputClassName, }) => { const { t, i18n } = useTranslation() + const docLink = useDocLink() const [changeKey, setChangeKey] = useState('') const handleFormChange = (key: string, val: string) => { @@ -57,7 +59,7 @@ const Form: FC<FormProps> = React.memo(({ </label> {variable === 'endpoint' && ( <a - href={'https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' || '/'} + href={docLink('/guides/knowledge-base/connect-external-knowledge-base') || '/'} target='_blank' rel='noopener noreferrer' className='body-xs-regular flex items-center text-text-accent' diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx index d4af34feaa..990fa5fcbc 100644 --- a/web/app/components/datasets/external-api/external-api-panel/index.tsx +++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx @@ -12,6 +12,7 @@ import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' import Loading from '@/app/components/base/loading' import { useModalContext } from '@/context/modal-context' +import { useDocLink } from '@/context/i18n' type ExternalAPIPanelProps = { onClose: () => void @@ -19,6 +20,7 @@ type ExternalAPIPanelProps = { const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose }) => { const { t } = useTranslation() + const docLink = useDocLink() const { setShowExternalKnowledgeAPIModal } = useModalContext() const { externalKnowledgeApiList, mutateExternalKnowledgeApis, isLoading } = useExternalKnowledgeApi() @@ -50,7 +52,8 @@ const ExternalAPIPanel: React.FC<ExternalAPIPanelProps> = ({ onClose }) => { <div className='flex grow flex-col items-start gap-1'> <div className='system-xl-semibold self-stretch text-text-primary'>{t('dataset.externalAPIPanelTitle')}</div> <div className='body-xs-regular self-stretch text-text-tertiary'>{t('dataset.externalAPIPanelDescription')}</div> - <a className='flex cursor-pointer items-center justify-center gap-1 self-stretch' href='https://docs.dify.ai/guides/knowledge-base/external-knowledge-api-documentation' target='_blank'> + <a className='flex cursor-pointer items-center justify-center gap-1 self-stretch' + href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target='_blank'> <RiBookOpenLine className='h-3 w-3 text-text-accent' /> <div className='body-xs-regular grow text-text-accent'>{t('dataset.externalAPIPanelDocumentation')}</div> </a> diff --git a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx index 5c3c1261b6..336e5dbb42 100644 --- a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx @@ -1,8 +1,10 @@ import { RiBookOpenLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' const InfoPanel = () => { const { t } = useTranslation() + const docLink = useDocLink() return ( <div className='flex w-[360px] flex-col items-start pb-2 pr-8 pt-[108px]'> @@ -16,12 +18,15 @@ const InfoPanel = () => { </span> <span className='system-sm-regular text-text-tertiary'> {t('dataset.connectDatasetIntro.content.front')} - <a className='system-sm-regular ml-1 text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/external-knowledge-api' target='_blank' rel="noopener noreferrer"> + <a className='system-sm-regular ml-1 text-text-accent' href={docLink('/guides/knowledge-base/external-knowledge-api')} target='_blank' rel="noopener noreferrer"> {t('dataset.connectDatasetIntro.content.link')} </a> {t('dataset.connectDatasetIntro.content.end')} </span> - <a className='system-sm-regular self-stretch text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base' target='_blank' rel="noopener noreferrer"> + <a className='system-sm-regular self-stretch text-text-accent' + href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} + target='_blank' + rel="noopener noreferrer"> {t('dataset.connectDatasetIntro.learnMore')} </a> </p> diff --git a/web/app/components/datasets/external-knowledge-base/create/index.tsx b/web/app/components/datasets/external-knowledge-base/create/index.tsx index 5fbddea06b..b8f754e9c4 100644 --- a/web/app/components/datasets/external-knowledge-base/create/index.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/index.tsx @@ -11,6 +11,7 @@ import InfoPanel from './InfoPanel' import type { CreateKnowledgeBaseReq } from './declarations' import Divider from '@/app/components/base/divider' import Button from '@/app/components/base/button' +import { useDocLink } from '@/context/i18n' type ExternalKnowledgeBaseCreateProps = { onConnect: (formValue: CreateKnowledgeBaseReq) => void @@ -19,6 +20,7 @@ type ExternalKnowledgeBaseCreateProps = { const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = ({ onConnect, loading }) => { const { t } = useTranslation() + const docLink = useDocLink() const router = useRouter() const [formData, setFormData] = useState<CreateKnowledgeBaseReq>({ name: '', @@ -59,7 +61,7 @@ const ExternalKnowledgeBaseCreate: React.FC<ExternalKnowledgeBaseCreateProps> = <span>{t('dataset.connectHelper.helper1')}</span> <span className='system-sm-medium text-text-secondary'>{t('dataset.connectHelper.helper2')}</span> <span>{t('dataset.connectHelper.helper3')}</span> - <a className='system-sm-regular self-stretch text-text-accent' href='https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base' target='_blank' rel="noopener noreferrer"> + <a className='system-sm-regular self-stretch text-text-accent' href={docLink('/guides/knowledge-base/connect-external-knowledge-base')} target='_blank' rel="noopener noreferrer"> {t('dataset.connectHelper.helper4')} </a> <span>{t('dataset.connectHelper.helper5')} </span> diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx index e66448b979..f65f395e30 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -11,6 +11,7 @@ import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/ec import Button from '@/app/components/base/button' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { useDocLink } from '@/context/i18n' type Props = { indexMethod: string @@ -29,6 +30,7 @@ const ModifyRetrievalModal: FC<Props> = ({ }) => { const ref = useRef(null) const { t } = useTranslation() + const docLink = useDocLink() const [retrievalConfig, setRetrievalConfig] = useState(value) // useClickAway(() => { @@ -72,7 +74,10 @@ const ModifyRetrievalModal: FC<Props> = ({ <a target='_blank' rel='noopener noreferrer' - href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' + href={docLink('/guides/knowledge-base/retrieval-test-and-citation#modify-text-retrieval-setting', { + 'zh-Hans': '/guides/knowledge-base/retrieval-test-and-citation#修改文本检索方式', + 'ja-JP': '/guides/knowledge-base/retrieval-test-and-citation', + })} className='text-text-accent' > {t('datasetSettings.form.retrievalSetting.learnMore')} diff --git a/web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts b/web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts index 45fe7675f1..aea7efd5c8 100644 --- a/web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts +++ b/web/app/components/datasets/metadata/hooks/use-check-metadata-name.ts @@ -18,6 +18,12 @@ const useCheckMetadataName = () => { } } + if (name.length > 255) { + return { + errorMsg: t(`${i18nPrefix}.tooLong`, { max: 255 }), + } + } + return { errorMsg: '', } diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index b317bfc887..b90e65a85b 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -32,6 +32,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro import { fetchMembers } from '@/service/common' import type { Member } from '@/models/common' import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle' +import { useDocLink } from '@/context/i18n' const rowClass = 'flex' const labelClass = ` @@ -46,6 +47,7 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { const Form = () => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useContext(ToastContext) const { mutate } = useSWRConfig() const { isCurrentWorkspaceDatasetOperator } = useAppContext() @@ -308,7 +310,16 @@ const Form = () => { <div> <div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div> <div className='body-xs-regular text-text-tertiary'> - <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a> + <a + target='_blank' + rel='noopener noreferrer' + href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting', { + 'zh-Hans': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#指定检索方式', + 'ja-JP': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#検索方法の指定', + })} + className='text-text-accent'> + {t('datasetSettings.form.retrievalSetting.learnMore')} + </a> {t('datasetSettings.form.retrievalSetting.description')} </div> </div> diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index 9682b51b39..f178645a8e 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -57,8 +57,8 @@ The text generation application offers non-session support and is ideal for tran <i>Due to Cloudflare restrictions, the request will be interrupted without a return after 100 seconds.</i> </Property> <Property name='user' type='string' key='user'> - User identifier, used to define the identity of the end-user for retrieval and statistics. - Should be uniquely defined by the developer within the application. + User identifier, used to define the identity of the end-user, convenient for retrieval and statistics. + The rules are defined by the developer and need to ensure that the user identifier is unique within the application. The Service API does not share conversations created by the WebApp. </Property> <Property name='files' type='array[object]' key='files'> File list, suitable for inputting files (images) combined with text understanding and answering questions, available only when the model supports Vision capability. @@ -220,7 +220,7 @@ The text generation application offers non-session support and is ideal for tran - `file` (File) Required The file to be uploaded. - `user` (string) Required - User identifier, defined by the developer's rules, must be unique within the application. + User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp. ### Response After a successful upload, the server will return the file's ID and related information. @@ -290,7 +290,7 @@ The text generation application offers non-session support and is ideal for tran - `task_id` (string) Task ID, can be obtained from the streaming chunk return Request Body - `user` (string) Required - User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. + User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. The Service API does not share conversations created by the WebApp. ### Response - `result` (string) Always returns "success" </Col> @@ -512,6 +512,8 @@ The text generation application offers non-session support and is ideal for tran - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) author name </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -528,7 +530,9 @@ The text generation application offers non-session support and is ideal for tran "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index fc6291f522..4dbefca8f8 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -220,7 +220,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `file` (File) 必須 アップロードするファイル。 - `user` (string) 必須 - 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 + 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。サービス API は WebApp によって作成された会話を共有しません。 ### レスポンス アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 @@ -289,7 +289,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `task_id` (string) タスク ID、ストリーミングチャンクの返信から取得可能 リクエストボディ - `user` (string) 必須 - ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。 + ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。サービス API は WebApp によって作成された会話を共有しません。 ### レスポンス - `result` (string) 常に"success"を返します </Col> @@ -510,6 +510,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -526,7 +528,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 9e65a4bd9b..4af5a28050 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -266,7 +266,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ### Request Body - `user` (string) Required - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。 ### Response - `result` (string) 固定返回 success </Col> @@ -485,6 +485,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -501,7 +503,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index 76e336bdf4..faecd3d1cd 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -59,7 +59,7 @@ Chat applications support session persistence, allowing previous chat history to </Property> <Property name='user' type='string' key='user'> User identifier, used to define the identity of the end-user for retrieval and statistics. - Should be uniquely defined by the developer within the application. + Should be uniquely defined by the developer within the application. The Service API does not share conversations created by the WebApp. </Property> <Property name='conversation_id' type='string' key='conversation_id'> Conversation ID, to continue the conversation based on previous chat records, it is necessary to pass the previous message's conversation_id. @@ -152,7 +152,6 @@ Chat applications support session persistence, allowing previous chat history to - `data` (object) detail - `id` (string) Unique ID of workflow execution - `workflow_id` (string) ID of related workflow - - `sequence_number` (int) Self-increasing serial number, self-increasing in the App, starting from 1 - `created_at` (timestamp) Creation timestamp, e.g., 1705395332 - `event: node_started` node execution started - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API @@ -287,7 +286,7 @@ Chat applications support session persistence, allowing previous chat history to ### Streaming Mode <CodeGroup title="Response"> ```streaming {{ title: 'Response' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -324,7 +323,7 @@ Chat applications support session persistence, allowing previous chat history to - `file` (File) Required The file to be uploaded. - `user` (string) Required - User identifier, defined by the developer's rules, must be unique within the application. + User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp. ### Response After a successful upload, the server will return the file's ID and related information. @@ -394,7 +393,7 @@ Chat applications support session persistence, allowing previous chat history to - `task_id` (string) Task ID, can be obtained from the streaming chunk return ### Request Body - `user` (string) Required - User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. + User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp. ### Response - `result` (string) Always returns "success" </Col> @@ -448,7 +447,7 @@ Chat applications support session persistence, allowing previous chat history to Upvote as `like`, downvote as `dislike`, revoke upvote as `null` </Property> <Property name='user' type='string' key='user'> - User identifier, defined by the developer's rules, must be unique within the application. + User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp. </Property> <Property name='content' type='string' key='content'> The specific content of message feedback. @@ -1123,6 +1122,8 @@ Chat applications support session persistence, allowing previous chat history to - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -1139,7 +1140,9 @@ Chat applications support session persistence, allowing previous chat history to "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx index 6f94cbb2c7..5ce54a61d2 100644 --- a/web/app/components/develop/template/template_advanced_chat.ja.mdx +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -59,7 +59,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from </Property> <Property name='user' type='string' key='user'> ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。 - アプリケーション内で開発者によって一意に定義されるべきです。 + アプリケーション内で開発者によって一意に定義されるべきです。サービス API は WebApp によって作成された会話を共有しません。 </Property> <Property name='conversation_id' type='string' key='conversation_id'> 会話ID、以前のチャット記録に基づいて会話を続けるには、以前のメッセージのconversation_idを渡す必要があります。 @@ -152,7 +152,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `data` (object) 詳細 - `id` (string) ワークフロー実行の一意ID - `workflow_id` (string) 関連ワークフローのID - - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 - `event: node_started` ノード実行が開始 - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 @@ -287,7 +286,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### ストリーミングモード <CodeGroup title="応答"> ```streaming {{ title: '応答' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -324,7 +323,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `file` (File) 必須 アップロードするファイル。 - `user` (string) 必須 - ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 @@ -394,7 +393,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます ### リクエストボディ - `user` (string) 必須 - ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 + ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 - `result` (string) 常に"success"を返します </Col> @@ -1123,6 +1122,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -1139,7 +1140,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx index 3e268d6e65..7a69ee60aa 100755 --- a/web/app/components/develop/template/template_advanced_chat.zh.mdx +++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx @@ -56,7 +56,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' </Property> <Property name='user' type='string' key='user'> 用户标识,用于定义终端用户的身份,方便检索、统计。 - 由开发者定义规则,需保证用户标识在应用内唯一。 + 由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。 </Property> <Property name='conversation_id' type='string' key='conversation_id'> (选填)会话 ID,需要基于之前的聊天记录继续对话,必须传之前消息的 conversation_id。 @@ -153,7 +153,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `data` (object) 详细内容 - `id` (string) workflow 执行 ID - `workflow_id` (string) 关联 Workflow ID - - `sequence_number` (int) 自增序号,App 内自增,从 1 开始 - `created_at` (timestamp) 开始时间 - `event: node_started` node 开始执行 - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 @@ -297,7 +296,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ### 流式模式 <CodeGroup title="Response"> ```streaming {{ title: 'Response' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -402,7 +401,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ### Request Body - `user` (string) Required - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。 ### Response - `result` (string) 固定返回 success </Col> @@ -1173,7 +1172,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index f0cb3cd513..c95471160e 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -82,9 +82,9 @@ Chat applications support session persistence, allowing previous chat history to ### ChatCompletionResponse Returns the complete App result, `Content-Type` is `application/json`. - - `event` (string) 事件类型,固定为 `message` - - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 - - `id` (string) 唯一ID + - `event` (string) Event type, always `message` in blocking mode. + - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API + - `id` (string) Unique ID, same as `message_id` - `message_id` (string) Unique message ID - `conversation_id` (string) Conversation ID - `mode` (string) App mode, fixed as `chat` @@ -287,7 +287,7 @@ Chat applications support session persistence, allowing previous chat history to - `file` (File) Required The file to be uploaded. - `user` (string) Required - User identifier, defined by the developer's rules, must be unique within the application. + User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp. ### Response After a successful upload, the server will return the file's ID and related information. @@ -357,7 +357,7 @@ Chat applications support session persistence, allowing previous chat history to - `task_id` (string) Task ID, can be obtained from the streaming chunk return ### Request Body - `user` (string) Required - User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. + User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp. ### Response - `result` (string) Always returns "success" </Col> @@ -1151,6 +1151,8 @@ Chat applications support session persistence, allowing previous chat history to - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -1167,7 +1169,9 @@ Chat applications support session persistence, allowing previous chat history to "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index a1b2ae5f1d..8368326e40 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -287,7 +287,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `file` (File) 必須 アップロードするファイル。 - `user` (string) 必須 - ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 @@ -357,7 +357,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得できます ### リクエストボディ - `user` (string) 必須 - ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。 + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 - `result` (string) 常に"success"を返します </Col> @@ -1150,6 +1150,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -1166,7 +1168,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index 9c1a168bf5..325470ac62 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -56,7 +56,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' </Property> <Property name='user' type='string' key='user'> 用户标识,用于定义终端用户的身份,方便检索、统计。 - 由开发者定义规则,需保证用户标识在应用内唯一。 + 由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。 </Property> <Property name='conversation_id' type='string' key='conversation_id'> (选填)会话 ID,需要基于之前的聊天记录继续对话,必须传之前消息的 conversation_id。 @@ -306,7 +306,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 要上传的文件。 </Property> <Property name='user' type='string' key='user'> - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。服务 API 不会共享 WebApp 创建的对话。 </Property> </Properties> @@ -373,7 +373,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' ### Request Body - `user` (string) Required - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。 ### Response - `result` (string) 固定返回 success </Col> @@ -425,7 +425,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' 点赞 like, 点踩 dislike, 撤销点赞 null </Property> <Property name='user' type='string' key='user'> - 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。 + 用户标识,由开发者定义规则,需保证用户标识在应用内唯一。服务 API 不会共享 WebApp 创建的对话。 </Property> <Property name='content' type='string' key='content'> 消息反馈的具体信息。 @@ -1162,6 +1162,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -1178,7 +1180,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index 95e4971e67..756cf20531 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -64,6 +64,8 @@ Workflow applications offers non-session support and is ideal for translation, a - `user` (string) Required User identifier, used to define the identity of the end-user for retrieval and statistics. Should be uniquely defined by the developer within the application. + <br/> + <i>The user identifier should be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp.</i> ### Response When `response_mode` is `blocking`, return a CompletionResponse object. @@ -101,7 +103,6 @@ Workflow applications offers non-session support and is ideal for translation, a - `data` (object) detail - `id` (string) Unique ID of workflow execution - `workflow_id` (string) ID of related workflow - - `sequence_number` (int) Self-increasing serial number, self-increasing in the App, starting from 1 - `created_at` (timestamp) Creation timestamp, e.g., 1705395332 - `event: node_started` node execution started - `task_id` (string) Task ID, used for request tracking and the below Stop Generate API @@ -239,7 +240,7 @@ Workflow applications offers non-session support and is ideal for translation, a ### Streaming Mode <CodeGroup title="Response"> ```streaming {{ title: 'Response' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -401,7 +402,7 @@ Workflow applications offers non-session support and is ideal for translation, a - `task_id` (string) Task ID, can be obtained from the streaming chunk return ### Request Body - `user` (string) Required - User identifier, used to define the identity of the end-user, must be consistent with the user passed in the send message interface. + User identifier, used to define the identity of the end-user, must be consistent with the user passed in the message sending interface. The Service API does not share conversations created by the WebApp. ### Response - `result` (string) Always returns "success" </Col> @@ -448,7 +449,7 @@ Workflow applications offers non-session support and is ideal for translation, a - `file` (File) Required The file to be uploaded. - `user` (string) Required - User identifier, defined by the developer's rules, must be unique within the application. + User identifier, defined by the developer's rules, must be unique within the application. The Service API does not share conversations created by the WebApp. ### Response After a successful upload, the server will return the file's ID and related information. @@ -531,6 +532,12 @@ Workflow applications offers non-session support and is ideal for translation, a <Property name='limit' type='int' key='limit'> How many chat history messages to return in one request, default is 20. </Property> + <Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'> + Created by which endUser, for example, `abc-123`. + </Property> + <Property name='created_by_account' type='str' key='created_by_account'> + Created by which email account, for example, lizb@test.com. + </Property> </Properties> ### Response @@ -625,6 +632,8 @@ Workflow applications offers non-session support and is ideal for translation, a - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -641,7 +650,9 @@ Workflow applications offers non-session support and is ideal for translation, a "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index ab53d05e81..e63393cbcd 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -104,7 +104,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `data` (object) 詳細 - `id` (string) ワークフロー実行の一意の ID - `workflow_id` (string) 関連するワークフローの ID - - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1 から始まります - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 - `event: node_started` ノード実行開始 - `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用 @@ -242,7 +241,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from ### ストリーミングモード <CodeGroup title="応答"> ```streaming {{ title: '応答' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -404,7 +403,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `task_id` (string) タスク ID、ストリーミングチャンクの返り値から取得可能 ### リクエストボディ - `user` (string) 必須 - ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 - `result` (string) 常に"success"を返します </Col> @@ -451,7 +450,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `file` (File) 必須 アップロードするファイル。 - `user` (string) 必須 - ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。サービス API は WebApp によって作成された会話を共有しません。 ### 応答 アップロードが成功すると、サーバーはファイルの ID と関連情報を返します。 @@ -534,6 +533,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from <Property name='limit' type='int' key='limit'> 1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20。 </Property> + <Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'> + どのendUserによって作成されたか、例えば、`abc-123`。 + </Property> + <Property name='created_by_account' type='str' key='created_by_account'> + どのメールアカウントによって作成されたか、例えば、lizb@test.com。 + </Property> </Properties> ### 応答 @@ -628,6 +633,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -644,7 +651,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index fe59988eda..fc193de5da 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -59,7 +59,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 <i>由于 Cloudflare 限制,请求会在 100 秒超时无返回后中断。</i> - `user` (string) Required 用户标识,用于定义终端用户的身份,方便检索、统计。 - 由开发者定义规则,需保证用户标识在应用内唯一。 + 由开发者定义规则,需保证用户标识在应用内唯一。API 无法访问 WebApp 创建的会话。 ### Response @@ -98,7 +98,6 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `data` (object) 详细内容 - `id` (string) workflow 执行 ID - `workflow_id` (string) 关联 Workflow ID - - `sequence_number` (int) 自增序号,App 内自增,从 1 开始 - `created_at` (timestamp) 开始时间 - `event: node_started` node 开始执行 - `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口 @@ -232,7 +231,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 ### Streaming Mode <CodeGroup title="Response"> ```streaming {{ title: 'Response' }} - data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "created_at": 1679586595}} data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} @@ -394,7 +393,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `task_id` (string) 任务 ID,可在流式返回 Chunk 中获取 ### Request Body - `user` (string) Required - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。API 无法访问 WebApp 创建的会话。 ### Response - `result` (string) 固定返回 "success" </Col> @@ -443,7 +442,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 要上传的文件。 </Property> <Property name='user' type='string' key='user'> - 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。 + 用户标识,用于定义终端用户的身份,必须和发送消息接口传入 user 保持一致。服务 API 不会共享 WebApp 创建的对话。 </Property> </Properties> @@ -522,6 +521,12 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 <Property name='limit' type='int' key='limit'> 每页条数, 默认20. </Property> + <Property name='created_by_end_user_session_id' type='str' key='created_by_end_user_session_id'> + 由哪个endUser创建,例如,`abc-123`. + </Property> + <Property name='created_by_account' type='str' key='created_by_account'> + 由哪个邮箱账户创建,例如,lizb@test.com. + </Property> </Properties> ### Response @@ -615,6 +620,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 </Col> <Col> <CodeGroup title="Request" tag="GET" label="/info" targetCode={`curl -X GET '${props.appDetail.api_base_url}/info' \\\n-H 'Authorization: Bearer {api_key}'`}> @@ -631,7 +638,9 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` </CodeGroup> diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 6648a4a2e8..9b36fc6020 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -23,7 +23,6 @@ import GithubStar from '../github-star' import Support from './support' import Compliance from './compliance' import PremiumBadge from '@/app/components/base/premium-badge' -import { useGetDocLanguage } from '@/context/i18n' import Avatar from '@/app/components/base/avatar' import ThemeSwitcher from '@/app/components/base/theme-switcher' import { logout } from '@/service/common' @@ -33,10 +32,11 @@ import { useModalContext } from '@/context/modal-context' import { IS_CLOUD_EDITION } from '@/config' import cn from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' +import { useDocLink } from '@/context/i18n' export default function AppSelector() { const itemClassName = ` - flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular + flex items-center w-full h-8 pl-3 pr-2 text-text-secondary system-md-regular rounded-lg hover:bg-state-base-hover cursor-pointer gap-1 ` const router = useRouter() @@ -44,10 +44,10 @@ export default function AppSelector() { const { systemFeatures } = useGlobalPublicStore() const { t } = useTranslation() + const docLink = useDocLink() const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() const { isEducationAccount } = useProviderContext() const { setShowAccountSettingModal } = useModalContext() - const docLanguage = useGetDocLanguage() const handleLogout = async () => { await logout({ @@ -87,24 +87,24 @@ export default function AppSelector() { backdrop-blur-sm focus:outline-none " > - <MenuItem disabled> - <div className='flex flex-nowrap items-center py-[13px] pl-3 pr-2'> - <div className='grow'> - <div className='system-md-medium break-all text-text-primary'> - {userProfile.name} - {isEducationAccount && ( - <PremiumBadge size='s' color='blue' className='ml-1 !px-2'> - <RiGraduationCapFill className='mr-1 h-3 w-3' /> - <span className='system-2xs-medium'>EDU</span> - </PremiumBadge> - )} + <div className="px-1 py-1"> + <MenuItem disabled> + <div className='flex flex-nowrap items-center py-2 pl-3 pr-2'> + <div className='grow'> + <div className='system-md-medium break-all text-text-primary'> + {userProfile.name} + {isEducationAccount && ( + <PremiumBadge size='s' color='blue' className='ml-1 !px-2'> + <RiGraduationCapFill className='mr-1 h-3 w-3' /> + <span className='system-2xs-medium'>EDU</span> + </PremiumBadge> + )} + </div> + <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> </div> - <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> + <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} /> </div> - <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' /> - </div> - </MenuItem> - <div className="px-1 py-1"> + </MenuItem> <MenuItem> <Link className={cn(itemClassName, 'group', @@ -133,7 +133,7 @@ export default function AppSelector() { className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover', )} - href={`https://docs.dify.ai/${docLanguage}/introduction`} + href={docLink('/introduction')} target='_blank' rel='noopener noreferrer'> <RiBookOpenLine className='size-4 shrink-0 text-text-tertiary' /> <div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.helpCenter')}</div> diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx index 98668c61d2..f384cbc0bc 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -31,22 +31,22 @@ const WorkplaceSelector = () => { } return ( - <Menu as="div" className="relative h-full w-full"> + <Menu as="div" className="min-w-0"> { ({ open }) => ( <> <MenuButton className={cn( ` group flex w-full cursor-pointer items-center - gap-1.5 p-0.5 hover:bg-state-base-hover ${open && 'bg-state-base-hover'} rounded-[10px] + p-0.5 hover:bg-state-base-hover ${open && 'bg-state-base-hover'} rounded-[10px] `, )}> - <div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'> + <div className='mr-1.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px] max-[800px]:mr-0'> <span className='h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle font-semibold uppercase leading-6 text-shadow-shadow-1 opacity-90'>{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span> </div> - <div className='flex flex-row'> - <div className={'system-sm-medium max-w-[160px] truncate text-text-secondary'}>{currentWorkspace?.name}</div> - <RiArrowDownSLine className='h-4 w-4 text-text-secondary' /> + <div className='flex min-w-0 items-center'> + <div className={'system-sm-medium min-w-0 max-w-[149px] truncate text-text-secondary max-[800px]:hidden'}>{currentWorkspace?.name}</div> + <RiArrowDownSLine className='h-4 w-4 shrink-0 text-text-secondary' /> </div> </MenuButton> <Transition @@ -59,10 +59,11 @@ const WorkplaceSelector = () => { leaveTo="transform opacity-0 scale-95" > <MenuItems + anchor="bottom start" className={cn( ` - shadows-shadow-lg absolute left-[-15px] mt-1 flex max-h-[400px] w-[280px] flex-col items-start overflow-y-auto rounded-xl - bg-components-panel-bg-blur backdrop-blur-[5px] + shadows-shadow-lg absolute left-[-15px] z-[1000] mt-1 flex max-h-[400px] w-[280px] flex-col items-start overflow-y-auto + rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px] `, )} > @@ -73,7 +74,7 @@ const WorkplaceSelector = () => { { workspaces.map(workspace => ( <div className='flex items-center gap-2 self-stretch rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}> - <div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'> + <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]'> <span className='h-6 bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle font-semibold uppercase leading-6 text-shadow-shadow-1 opacity-90'>{workspace?.name[0]?.toLocaleUpperCase()}</span> </div> <div className='system-md-regular line-clamp-1 grow cursor-pointer overflow-hidden text-ellipsis text-text-secondary'>{workspace.name}</div> diff --git a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx index a7c73917bb..c95e8d8d0c 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx @@ -3,9 +3,11 @@ import { RiExternalLinkLine, RiPuzzle2Line, } from '@remixicon/react' +import { useDocLink } from '@/context/i18n' const Empty = () => { const { t } = useTranslation() + const docLink = useDocLink() return ( <div className='mb-2 rounded-xl bg-background-section p-6'> @@ -15,7 +17,7 @@ const Empty = () => { <div className='system-sm-medium mb-1 text-text-secondary'>{t('common.apiBasedExtension.title')}</div> <a className='system-xs-regular flex items-center text-text-accent' - href={t('common.apiBasedExtension.linkUrl') || '/'} + href={docLink('/guides/extension/api-based-extension/README')} target='_blank' rel='noopener noreferrer' > {t('common.apiBasedExtension.link')} diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index 53f7673b15..f6c0d93db0 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' @@ -29,6 +30,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ onSave, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [localeData, setLocaleData] = useState(data) const [loading, setLoading] = useState(false) const { notify } = useToastContext() @@ -100,7 +102,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ <div className='flex h-9 items-center justify-between text-sm font-medium text-text-primary'> {t('common.apiBasedExtension.modal.apiEndpoint.title')} <a - href={t('common.apiBasedExtension.linkUrl') || '/'} + href={docLink('/guides/extension/api-based-extension/README')} target='_blank' rel='noopener noreferrer' className='group flex items-center text-xs font-normal text-text-accent' > diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index 38efdcb1b7..065ef91eba 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -9,6 +9,8 @@ import { useAppContext } from '@/context/app-context' import { fetchNotionConnection } from '@/service/common' import NotionIcon from '@/app/components/base/notion-icon' import { noop } from 'lodash-es' +import { useTranslation } from 'react-i18next' +import Toast from '@/app/components/base/toast' const Icon: FC<{ src: string @@ -33,6 +35,7 @@ const DataSourceNotion: FC<Props> = ({ const { isCurrentWorkspaceManager } = useAppContext() const [canConnectNotion, setCanConnectNotion] = useState(false) const { data } = useSWR(canConnectNotion ? '/oauth/data-source/notion' : null, fetchNotionConnection) + const { t } = useTranslation() const connected = !!workspaces.length @@ -51,9 +54,19 @@ const DataSourceNotion: FC<Props> = ({ } useEffect(() => { - if (data?.data) - window.location.href = data.data - }, [data]) + if (data && 'data' in data) { + if (data.data && typeof data.data === 'string' && data.data.startsWith('http')) { + window.location.href = data.data + } + else if (data.data === 'internal') { + Toast.notify({ + type: 'info', + message: t('common.dataSource.notion.integratedAlert'), + }) + } + } + }, [data, t]) + return ( <Panel type={DataSourceType.notion} diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index df7aae6b62..eb8a0291de 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -70,7 +70,6 @@ const MembersPage = () => { {isCurrentWorkspaceOwner && <span> <Tooltip popupContent={t('common.account.editWorkspaceInfo')} - needsDelay > <div className='cursor-pointer rounded-md p-1 hover:bg-black/5' diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index caf34131b7..1f5ced612c 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -1,3 +1,5 @@ +import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' + export type FormValue = Record<string, any> export type TypeWithI18N<T = string> = { @@ -20,12 +22,16 @@ export enum FormTypeEnum { multiToolSelector = 'array[tools]', appSelector = 'app-selector', any = 'any', + object = 'object', + array = 'array', + dynamicSelect = 'dynamic-select', } export type FormOption = { label: TypeWithI18N value: string show_on: FormShowOnObject[] + icon?: string } export enum ModelTypeEnum { @@ -108,6 +114,7 @@ export type FormShowOnObject = { } export type CredentialFormSchemaBase = { + name: string variable: string label: TypeWithI18N type: FormTypeEnum @@ -117,6 +124,7 @@ export type CredentialFormSchemaBase = { show_on: FormShowOnObject[] url?: string scope?: string + input_schema?: SchemaRoot } export type CredentialFormSchemaTextInput = CredentialFormSchemaBase & { diff --git a/web/app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx b/web/app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx index 818007a26f..c60a769e40 100644 --- a/web/app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx +++ b/web/app/components/header/account-setting/model-provider-page/install-from-marketplace.tsx @@ -17,9 +17,9 @@ import Loading from '@/app/components/base/loading' import ProviderCard from '@/app/components/plugins/provider-card' import List from '@/app/components/plugins/marketplace/list' import type { Plugin } from '@/app/components/plugins/types' -import { MARKETPLACE_URL_PREFIX } from '@/config' import cn from '@/utils/classnames' import { getLocaleOnClient } from '@/i18n' +import { getMarketplaceUrl } from '@/utils/var' type InstallFromMarketplaceProps = { providers: ModelProvider[] @@ -55,7 +55,7 @@ const InstallFromMarketplace = ({ </div> <div className='mb-2 flex items-center pt-2'> <span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span> - <Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}${theme ? `?theme=${theme}` : ''}`} className='system-sm-medium inline-flex items-center text-text-accent'> + <Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'> {t('plugin.marketplace.difyMarketplace')} <RiArrowRightUpLine className='h-4 w-4' /> </Link> diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx index 14a56ca27b..3b3527a9fd 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx @@ -55,6 +55,7 @@ type FormProps< nodeId?: string nodeOutputVars?: NodeOutPutVar[], availableNodes?: Node[], + canChooseMCPTool?: boolean } function Form< @@ -80,6 +81,7 @@ function Form< nodeId, nodeOutputVars, availableNodes, + canChooseMCPTool, }: FormProps<CustomFormSchema>) { const language = useLanguage() const [changeKey, setChangeKey] = useState('') @@ -378,6 +380,7 @@ function Form< value={value[variable] || []} onChange={item => handleFormChange(variable, item as any)} supportCollapse + canChooseMCPTool={canChooseMCPTool} /> {fieldMoreInfo?.(formSchema)} {validating && changeKey === variable && <ValidatingTip />} diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx index 02b8cb2448..259a686d6f 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx @@ -63,7 +63,6 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa '/plugins', )} asChild={false} - needsDelay={true} > <RiErrorWarningFill className='h-4 w-4 text-text-destructive' /> </Tooltip> @@ -87,7 +86,6 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa '/plugins', )} asChild={false} - needsDelay > <RiErrorWarningFill className='h-4 w-4 text-text-destructive' /> </Tooltip> diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 62ad4252aa..8908d9a039 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -48,7 +48,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad <div key={model.model} className={classNames( - 'group flex items-center pl-2 pr-2.5 h-8 rounded-lg', + 'group flex h-8 items-center rounded-lg pl-2 pr-2.5', isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover', model.deprecated && 'opacity-60', )} @@ -105,7 +105,6 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad popupContent={ <span className='font-semibold'>{t('common.modelProvider.modelHasBeenDeprecated')}</span>} offset={{ mainAxis: 4 } } - needsDelay > <Switch defaultValue={false} disabled size='md' /> </Tooltip> diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index 3b902811fc..4934ed3105 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -96,7 +96,7 @@ const AppNav = () => { link, } }) - setNavItems(navItems) + setNavItems(navItems as any) } }, [appsData, isCurrentWorkspaceEditor, setNavItems]) @@ -122,7 +122,7 @@ const AppNav = () => { text={t('common.menus.apps')} activeSegment={['apps', 'app']} link='/apps' - curNav={appDetail} + curNav={appDetail as any} navs={navItems} createText={t('common.menus.newApp')} onCreate={openModal} diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx index 4894a62484..85223f9f37 100644 --- a/web/app/components/header/dataset-nav/index.tsx +++ b/web/app/components/header/dataset-nav/index.tsx @@ -48,7 +48,7 @@ const DatasetNav = () => { text={t('common.menus.datasets')} activeSegment='datasets' link='/datasets' - curNav={currentDataset as Omit<NavItem, 'link'>} + curNav={currentDataset as any} navs={datasetItems.map(dataset => ({ id: dataset.id, name: dataset.name, @@ -59,6 +59,7 @@ const DatasetNav = () => { createText={t('common.menus.newDataset')} onCreate={() => router.push(`${basePath}/datasets/create`)} onLoadmore={handleLoadmore} + isApp={false} /> ) } diff --git a/web/app/components/header/env-nav/index.tsx b/web/app/components/header/env-nav/index.tsx index cec933a4c5..3f0b0f01dd 100644 --- a/web/app/components/header/env-nav/index.tsx +++ b/web/app/components/header/env-nav/index.tsx @@ -20,22 +20,22 @@ const EnvNav = () => { return ( <div className={` - mr-4 flex h-[22px] items-center rounded-md border px-2 text-xs font-medium + mr-1 flex h-[22px] items-center rounded-md border px-2 text-xs font-medium ${headerEnvClassName[langeniusVersionInfo.current_env]} `}> { langeniusVersionInfo.current_env === 'TESTING' && ( <> - <Beaker02 className='mr-1 h-3 w-3' /> - {t('common.environment.testing')} + <Beaker02 className='h-3 w-3' /> + <div className='ml-1 max-[1280px]:hidden'>{t('common.environment.testing')}</div> </> ) } { langeniusVersionInfo.current_env === 'DEVELOPMENT' && ( <> - <TerminalSquare className='mr-1 h-3 w-3' /> - {t('common.environment.development')} + <TerminalSquare className='h-3 w-3' /> + <div className='ml-1 max-[1280px]:hidden'>{t('common.environment.development')}</div> </> ) } diff --git a/web/app/components/header/explore-nav/index.tsx b/web/app/components/header/explore-nav/index.tsx index b6ebf5d1a9..6896722a84 100644 --- a/web/app/components/header/explore-nav/index.tsx +++ b/web/app/components/header/explore-nav/index.tsx @@ -27,10 +27,12 @@ const ExploreNav = ({ )}> { activated - ? <RiPlanetFill className='mr-2 h-4 w-4' /> - : <RiPlanetLine className='mr-2 h-4 w-4' /> + ? <RiPlanetFill className='h-4 w-4' /> + : <RiPlanetLine className='h-4 w-4' /> } - {t('common.menus.explore')} + <div className='ml-2 max-[1024px]:hidden'> + {t('common.menus.explore')} + </div> </Link> ) } diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index dd0ec77b82..6486e3707a 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -1,6 +1,8 @@ 'use client' +import React, { useState } from 'react' import { usePathname } from 'next/navigation' import s from './index.module.css' +import { useEventEmitterContextContext } from '@/context/event-emitter' import classNames from '@/utils/classnames' type HeaderWrapperProps = { @@ -12,12 +14,23 @@ const HeaderWrapper = ({ }: HeaderWrapperProps) => { const pathname = usePathname() const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools'].includes(pathname) + // Check if the current path is a workflow canvas & fullscreen + const inWorkflowCanvas = pathname.endsWith('/workflow') + const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' + const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) + const { eventEmitter } = useEventEmitterContextContext() + + eventEmitter?.useSubscription((v: any) => { + if (v?.type === 'workflow-canvas-maximize') + setHideHeader(v.payload) + }) return ( <div className={classNames( - 'sticky top-0 left-0 right-0 z-30 flex flex-col grow-0 shrink-0 basis-auto min-h-[56px]', + 'sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col', s.header, isBordered ? 'border-b border-divider-regular' : '', + hideHeader && inWorkflowCanvas && 'hidden', )} > {children} diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index a9c26e0070..fc511d2954 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -1,9 +1,6 @@ 'use client' -import { useCallback, useEffect } from 'react' +import { useCallback } from 'react' import Link from 'next/link' -import { useBoolean } from 'ahooks' -import { useSelectedLayoutSegment } from 'next/navigation' -import { Bars3Icon } from '@heroicons/react/20/solid' import AccountDropdown from './account-dropdown' import AppNav from './app-nav' import DatasetNav from './dataset-nav' @@ -24,17 +21,15 @@ import { Plan } from '../billing/type' import { useGlobalPublicStore } from '@/context/global-public-context' const navClassName = ` - flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl + flex items-center relative px-3 h-8 rounded-xl font-medium text-sm cursor-pointer ` const Header = () => { const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() - const selectedSegment = useSelectedLayoutSegment() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false) const { enableBilling, plan } = useProviderContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -46,23 +41,12 @@ const Header = () => { setShowAccountSettingModal({ payload: 'billing' }) }, [isFreePlan, setShowAccountSettingModal, setShowPricingModal]) - useEffect(() => { - hideNavMenu() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedSegment]) - return ( - <div className='relative flex flex-1 items-center justify-between bg-background-body'> - <div className='flex items-center'> - {isMobile && <div - className='flex h-8 w-8 cursor-pointer items-center justify-center' - onClick={toggle} - > - <Bars3Icon className="h-4 w-4 text-gray-500" /> - </div>} - { - !isMobile - && <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'> - <Link href="/apps" className='flex h-8 shrink-0 items-center justify-center gap-2 px-0.5'> + if (isMobile) { + return ( + <div className=''> + <div className='flex items-center justify-between px-2'> + <div className='flex items-center'> + <Link href="/apps" className='flex h-8 shrink-0 items-center justify-center px-0.5'> {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? <img src={systemFeatures.branding.workspace_logo} @@ -71,59 +55,61 @@ const Header = () => { /> : <DifyLogo />} </Link> - <div className='font-light text-divider-deep'>/</div> - <div className='flex items-center gap-0.5'> - <WorkspaceProvider> - <WorkplaceSelector /> - </WorkspaceProvider> - {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />} - </div> + <div className='mx-1.5 shrink-0 font-light text-divider-deep'>/</div> + <WorkspaceProvider> + <WorkplaceSelector /> + </WorkspaceProvider> + {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />} </div> - } - </div > - {isMobile && ( - <div className='flex'> - <Link href="/apps" className='mr-4 flex items-center'> - {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo - ? <img - src={systemFeatures.branding.workspace_logo} - className='block h-[22px] w-auto object-contain' - alt='logo' - /> - : <DifyLogo />} - </Link> - <div className='font-light text-divider-deep'>/</div> - {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />} - </div > - )} - { - !isMobile && ( - <div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center'> - {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} - {!isCurrentWorkspaceDatasetOperator && <AppNav />} - {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} - {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} + <div className='flex items-center'> + <div className='mr-2'> + <PluginsNav /> + </div> + <AccountDropdown /> </div> - ) - } - <div className='flex shrink-0 items-center pr-3'> + </div> + <div className='my-1 flex items-center justify-center space-x-1'> + {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} + {!isCurrentWorkspaceDatasetOperator && <AppNav />} + {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} + {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} + </div> + </div> + ) + } + + return ( + <div className='flex h-[56px] items-center'> + <div className='flex min-w-0 flex-[1] items-center pl-3 pr-2 min-[1280px]:pr-3'> + <Link href="/apps" className='flex h-8 shrink-0 items-center justify-center px-0.5'> + {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo + ? <img + src={systemFeatures.branding.workspace_logo} + className='block h-[22px] w-auto object-contain' + alt='logo' + /> + : <DifyLogo />} + </Link> + <div className='mx-1.5 shrink-0 font-light text-divider-deep'>/</div> + <WorkspaceProvider> + <WorkplaceSelector /> + </WorkspaceProvider> + {enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />} + </div> + <div className='flex items-center space-x-2'> + {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} + {!isCurrentWorkspaceDatasetOperator && <AppNav />} + {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} + {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} + </div> + <div className='flex min-w-0 flex-[1] items-center justify-end pl-2 pr-3 min-[1280px]:pl-3'> <EnvNav /> <div className='mr-2'> <PluginsNav /> </div> <AccountDropdown /> </div> - { - (isMobile && isShowNavMenu) && ( - <div className='flex w-full flex-col gap-y-1 p-2'> - {!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />} - {!isCurrentWorkspaceDatasetOperator && <AppNav />} - {(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />} - {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} - </div> - ) - } - </div > + </div> ) } export default Header diff --git a/web/app/components/header/nav/index.tsx b/web/app/components/header/nav/index.tsx index 293c66a7b0..70cbd6d37b 100644 --- a/web/app/components/header/nav/index.tsx +++ b/web/app/components/header/nav/index.tsx @@ -46,7 +46,7 @@ const Nav = ({ return ( <div className={` - mr-0 flex h-8 shrink-0 items-center rounded-xl px-0.5 text-sm font-medium sm:mr-3 + flex h-8 max-w-[670px] shrink-0 items-center rounded-xl px-0.5 text-sm font-medium max-[1024px]:max-w-[400px] ${isActivated && 'bg-components-main-nav-nav-button-bg-active font-semibold shadow-md'} ${!curNav && !isActivated && 'hover:bg-components-main-nav-nav-button-bg-hover'} `}> @@ -54,14 +54,14 @@ const Nav = ({ <div onClick={() => setAppDetail()} className={classNames(` - flex items-center h-7 px-2.5 cursor-pointer rounded-[10px] + flex h-7 cursor-pointer items-center rounded-[10px] px-2.5 ${isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text'} ${curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover'} `)} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} > - <div className='mr-2'> + <div> { (hovered && curNav) ? <ArrowNarrowLeft className='h-4 w-4' /> @@ -70,7 +70,9 @@ const Nav = ({ : icon } </div> - {text} + <div className='ml-2 max-[1024px]:hidden'> + {text} + </div> </div> </Link> { diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 473abf03c1..77cf348da2 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -53,136 +53,134 @@ const NavSelector = ({ curNav, navs, createText, isApp, onCreate, onLoadmore }: }, 50), []) return ( - <div className=""> - <Menu as="div" className="relative inline-block text-left"> - {({ open }) => ( - <> - <MenuButton className={cn( - 'hover:hover:bg-components-main-nav-nav-button-bg-active-hover group inline-flex h-7 w-full items-center justify-center rounded-[10px] pl-2 pr-2.5 text-[14px] font-semibold text-components-main-nav-nav-button-text-active', - open && 'bg-components-main-nav-nav-button-bg-active', - )}> - <div className='max-w-[180px] truncate' title={curNav?.name}>{curNav?.name}</div> - <RiArrowDownSLine - className={cn('ml-1 h-3 w-3 shrink-0 opacity-50 group-hover:opacity-100', open && '!opacity-100')} - aria-hidden="true" - /> - </MenuButton> - <MenuItems - className=" - absolute -left-11 right-0 mt-1.5 w-60 max-w-80 - origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur - shadow-lg - " - > - <div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}> - { - navs.map(nav => ( - <MenuItem key={nav.id}> - <div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-text-secondary hover:bg-state-base-hover' onClick={() => { - if (curNav?.id === nav.id) - return - setAppDetail() - router.push(nav.link) - }} title={nav.name}> - <div className='relative mr-2 h-6 w-6 rounded-md'> - <AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} /> - {!!nav.mode && ( - <span className={cn( - 'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm', - )}> - {nav.mode === 'advanced-chat' && ( - <ChatBot className='h-2.5 w-2.5 text-[#1570EF]' /> - )} - {nav.mode === 'agent-chat' && ( - <CuteRobot className='h-2.5 w-2.5 text-indigo-600' /> - )} - {nav.mode === 'chat' && ( - <ChatBot className='h-2.5 w-2.5 text-[#1570EF]' /> - )} - {nav.mode === 'completion' && ( - <AiText className='h-2.5 w-2.5 text-[#0E9384]' /> - )} - {nav.mode === 'workflow' && ( - <Route className='h-2.5 w-2.5 text-[#f79009]' /> - )} - </span> - )} - </div> - <div className='truncate'> - {nav.name} - </div> + <Menu as="div" className="relative"> + {({ open }) => ( + <> + <MenuButton className={cn( + 'hover:hover:bg-components-main-nav-nav-button-bg-active-hover group inline-flex h-7 w-full items-center justify-center rounded-[10px] pl-2 pr-2.5 text-[14px] font-semibold text-components-main-nav-nav-button-text-active', + open && 'bg-components-main-nav-nav-button-bg-active', + )}> + <div className='max-w-[157px] truncate' title={curNav?.name}>{curNav?.name}</div> + <RiArrowDownSLine + className={cn('ml-1 h-3 w-3 shrink-0 opacity-50 group-hover:opacity-100', open && '!opacity-100')} + aria-hidden="true" + /> + </MenuButton> + <MenuItems + className=" + absolute -left-11 right-0 mt-1.5 w-60 max-w-80 + origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur + shadow-lg + " + > + <div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}> + { + navs.map(nav => ( + <MenuItem key={nav.id}> + <div className='flex w-full cursor-pointer items-center truncate rounded-lg px-3 py-[6px] text-[14px] font-normal text-text-secondary hover:bg-state-base-hover' onClick={() => { + if (curNav?.id === nav.id) + return + setAppDetail() + router.push(nav.link) + }} title={nav.name}> + <div className='relative mr-2 h-6 w-6 rounded-md'> + <AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} /> + {!!nav.mode && ( + <span className={cn( + 'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm', + )}> + {nav.mode === 'advanced-chat' && ( + <ChatBot className='h-2.5 w-2.5 text-[#1570EF]' /> + )} + {nav.mode === 'agent-chat' && ( + <CuteRobot className='h-2.5 w-2.5 text-indigo-600' /> + )} + {nav.mode === 'chat' && ( + <ChatBot className='h-2.5 w-2.5 text-[#1570EF]' /> + )} + {nav.mode === 'completion' && ( + <AiText className='h-2.5 w-2.5 text-[#0E9384]' /> + )} + {nav.mode === 'workflow' && ( + <Route className='h-2.5 w-2.5 text-[#f79009]' /> + )} + </span> + )} + </div> + <div className='truncate'> + {nav.name} </div> - </MenuItem> - )) - } - </div> - {!isApp && isCurrentWorkspaceEditor && ( - <MenuItem as="div" className='w-full p-1'> - <div onClick={() => onCreate('')} className={cn( - 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ', - )}> - <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'> - <RiAddLine className='h-4 w-4 text-text-primary' /> </div> - <div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div> + </MenuItem> + )) + } + </div> + {!isApp && isCurrentWorkspaceEditor && ( + <MenuItem as="div" className='w-full p-1'> + <div onClick={() => onCreate('')} className={cn( + 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover ', + )}> + <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'> + <RiAddLine className='h-4 w-4 text-text-primary' /> </div> - </MenuItem> - )} - {isApp && isCurrentWorkspaceEditor && ( - <Menu as="div" className="relative h-full w-full"> - {({ open }) => ( - <> - <MenuButton className='w-full p-1'> - <div className={cn( - 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover', - open && '!bg-state-base-hover', - )}> - <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'> - <RiAddLine className='h-4 w-4 text-text-primary' /> - </div> - <div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div> - <RiArrowRightSLine className='h-3.5 w-3.5 shrink-0 text-text-primary' /> + <div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div> + </div> + </MenuItem> + )} + {isApp && isCurrentWorkspaceEditor && ( + <Menu as="div" className="relative h-full w-full"> + {({ open }) => ( + <> + <MenuButton className='w-full p-1'> + <div className={cn( + 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover', + open && '!bg-state-base-hover', + )}> + <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border-[0.5px] border-divider-regular bg-background-default'> + <RiAddLine className='h-4 w-4 text-text-primary' /> </div> - </MenuButton> - <Transition - as={Fragment} - enter="transition ease-out duration-100" - enterFrom="transform opacity-0 scale-95" - enterTo="transform opacity-100 scale-100" - leave="transition ease-in duration-75" - leaveFrom="transform opacity-100 scale-100" - leaveTo="transform opacity-0 scale-95" - > - <MenuItems className={cn( - 'absolute right-[-198px] top-[3px] z-10 min-w-[200px] rounded-lg bg-components-panel-bg-blur shadow-lg', - )}> - <div className='p-1'> - <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('blank')}> - <FilePlus01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> - {t('app.newApp.startFromBlank')} - </div> - <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('template')}> - <FilePlus02 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> - {t('app.newApp.startFromTemplate')} - </div> + <div className='grow text-left text-[14px] font-normal text-text-secondary'>{createText}</div> + <RiArrowRightSLine className='h-3.5 w-3.5 shrink-0 text-text-primary' /> + </div> + </MenuButton> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <MenuItems className={cn( + 'absolute right-[-198px] top-[3px] z-10 min-w-[200px] rounded-lg bg-components-panel-bg-blur shadow-lg', + )}> + <div className='p-1'> + <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('blank')}> + <FilePlus01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> + {t('app.newApp.startFromBlank')} + </div> + <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('template')}> + <FilePlus02 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> + {t('app.newApp.startFromTemplate')} </div> - <div className='border-t border-divider-regular p-1'> - <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('dsl')}> - <FileArrow01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> - {t('app.importDSL')} - </div> + </div> + <div className='border-t border-divider-regular p-1'> + <div className={cn('flex cursor-pointer items-center rounded-lg px-3 py-[6px] font-normal text-text-secondary hover:bg-state-base-hover')} onClick={() => onCreate('dsl')}> + <FileArrow01 className='mr-2 h-4 w-4 shrink-0 text-text-secondary' /> + {t('app.importDSL')} </div> - </MenuItems> - </Transition> - </> - )} - </Menu> - )} - </MenuItems> - </> - )} - </Menu> - </div> + </div> + </MenuItems> + </Transition> + </> + )} + </Menu> + )} + </MenuItems> + </> + )} + </Menu> ) } diff --git a/web/app/components/header/plugins-nav/index.tsx b/web/app/components/header/plugins-nav/index.tsx index b22e717c94..7b28e27639 100644 --- a/web/app/components/header/plugins-nav/index.tsx +++ b/web/app/components/header/plugins-nav/index.tsx @@ -31,8 +31,8 @@ const PluginsNav = ({ )}> <div className={classNames( - 'relative flex flex-row h-8 p-1.5 gap-0.5 border border-transparent items-center justify-center rounded-xl system-sm-medium-uppercase', - activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active shadow-md text-components-main-nav-nav-button-text', + 'system-sm-medium relative flex h-8 flex-row items-center justify-center gap-0.5 rounded-xl border border-transparent p-1.5', + activated && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text shadow-md', !activated && 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', (isInstallingWithError || isFailed) && !activated && 'border-components-panel-border-subtle', )} diff --git a/web/app/components/header/tools-nav/index.tsx b/web/app/components/header/tools-nav/index.tsx index b87398708c..eb8d806c02 100644 --- a/web/app/components/header/tools-nav/index.tsx +++ b/web/app/components/header/tools-nav/index.tsx @@ -22,16 +22,18 @@ const ToolsNav = ({ return ( <Link href="/tools" className={classNames( 'group text-sm font-medium', - activated && 'font-semibold bg-components-main-nav-nav-button-bg-active hover:bg-components-main-nav-nav-button-bg-active-hover shadow-md', + activated && 'hover:bg-components-main-nav-nav-button-bg-active-hover bg-components-main-nav-nav-button-bg-active font-semibold shadow-md', activated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text hover:bg-components-main-nav-nav-button-bg-hover', className, )}> { activated - ? <RiHammerFill className='mr-2 h-4 w-4' /> - : <RiHammerLine className='mr-2 h-4 w-4' /> + ? <RiHammerFill className='h-4 w-4' /> + : <RiHammerLine className='h-4 w-4' /> } - {t('common.menus.tools')} + <div className='ml-2 max-[1024px]:hidden'> + {t('common.menus.tools')} + </div> </Link> ) } diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 1cc18ac24f..bdb3705f6f 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -15,6 +15,7 @@ import { renderI18nObject } from '@/i18n' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' import Partner from '../base/badges/partner' import Verified from '../base/badges/verified' +import { RiAlertFill } from '@remixicon/react' export type Props = { className?: string @@ -28,6 +29,7 @@ export type Props = { isLoading?: boolean loadingFileName?: string locale?: string + limitedInstall?: boolean } const Card = ({ @@ -42,6 +44,7 @@ const Card = ({ isLoading = false, loadingFileName, locale: localeFromProps, + limitedInstall = false, }: Props) => { const defaultLocale = useGetLanguage() const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale @@ -54,7 +57,7 @@ const Card = ({ obj ? renderI18nObject(obj, locale) : '' const isPartner = badges.includes('partner') - const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 pb-3 shadow-xs', className) + const wrapClassName = cn('hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', className) if (isLoading) { return ( <Placeholder @@ -66,30 +69,39 @@ const Card = ({ return ( <div className={wrapClassName}> - {!hideCornerMark && <CornerMark text={cornerMark} />} - {/* Header */} - <div className="flex"> - <Icon src={icon} installed={installed} installFailed={installFailed} /> - <div className="ml-3 w-0 grow"> - <div className="flex h-5 items-center"> - <Title title={getLocalizedText(label)} /> - {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} - {verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} - {titleLeft} {/* This can be version badge */} + <div className={cn('p-4 pb-3', limitedInstall && 'pb-1')}> + {!hideCornerMark && <CornerMark text={cornerMark} />} + {/* Header */} + <div className="flex"> + <Icon src={icon} installed={installed} installFailed={installFailed} /> + <div className="ml-3 w-0 grow"> + <div className="flex h-5 items-center"> + <Title title={getLocalizedText(label)} /> + {isPartner && <Partner className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.partnerTip')} />} + {verified && <Verified className='ml-0.5 h-4 w-4' text={t('plugin.marketplace.verifiedTip')} />} + {titleLeft} {/* This can be version badge */} + </div> + <OrgInfo + className="mt-0.5" + orgName={org} + packageName={name} + /> </div> - <OrgInfo - className="mt-0.5" - orgName={org} - packageName={name} - /> </div> + <Description + className="mt-3" + text={getLocalizedText(brief)} + descriptionLineRows={descriptionLineRows} + /> + {footer && <div>{footer}</div>} </div> - <Description - className="mt-3" - text={getLocalizedText(brief)} - descriptionLineRows={descriptionLineRows} - /> - {footer && <div>{footer}</div>} + {limitedInstall + && <div className='relative flex h-8 items-center gap-x-2 px-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:top-0 after:bg-toast-warning-bg after:opacity-40'> + <RiAlertFill className='h-3 w-3 shrink-0 text-text-warning-secondary' /> + <p className='system-xs-regular z-10 grow text-text-secondary'> + {t('plugin.installModal.installWarning')} + </p> + </div>} </div> ) } diff --git a/web/app/components/plugins/install-plugin/base/use-get-icon.ts b/web/app/components/plugins/install-plugin/base/use-get-icon.ts index bb46f27f53..23d41aff07 100644 --- a/web/app/components/plugins/install-plugin/base/use-get-icon.ts +++ b/web/app/components/plugins/install-plugin/base/use-get-icon.ts @@ -1,11 +1,11 @@ import { useCallback } from 'react' -import { apiPrefix } from '@/config' +import { API_PREFIX } from '@/config' import { useSelector } from '@/context/app-context' const useGetIcon = () => { const currentWorkspace = useSelector(s => s.currentWorkspace) const getIconUrl = useCallback((fileName: string) => { - return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}` + return `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}` }, [currentWorkspace.id]) return { diff --git a/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx b/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx new file mode 100644 index 0000000000..d668cb3d12 --- /dev/null +++ b/web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx @@ -0,0 +1,46 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import type { SystemFeatures } from '@/types/feature' +import { InstallationScope } from '@/types/feature' +import type { Plugin, PluginManifestInMarket } from '../../types' + +type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' } + +export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFeatures) { + if (systemFeatures.plugin_installation_permission.restrict_to_marketplace_only) { + if (plugin.from === 'github' || plugin.from === 'package') + return { canInstall: false } + } + + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.ALL) { + return { + canInstall: true, + } + } + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.NONE) { + return { + canInstall: false, + } + } + const verification = plugin.verification || {} + if (!plugin.verification || !plugin.verification.authorized_category) + verification.authorized_category = 'langgenius' + + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_ONLY) { + return { + canInstall: verification.authorized_category === 'langgenius', + } + } + if (systemFeatures.plugin_installation_permission.plugin_installation_scope === InstallationScope.OFFICIAL_AND_PARTNER) { + return { + canInstall: verification.authorized_category === 'langgenius' || verification.authorized_category === 'partner', + } + } + return { + canInstall: true, + } +} + +export default function usePluginInstallLimit(plugin: PluginProps) { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + return pluginInstallLimit(plugin, systemFeatures) +} diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx index 96abaa2e1c..48f0bff3c7 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx @@ -39,7 +39,7 @@ const Item: FC<Props> = ({ plugin_id: data.unique_identifier, } onFetchedPayload(payload) - setPayload(payload) + setPayload({ ...payload, from: dependency.type }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]) diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx index 5eb4c94abe..cf336ae682 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx @@ -8,6 +8,7 @@ import useGetIcon from '../../base/use-get-icon' import { MARKETPLACE_API_PREFIX } from '@/config' import Version from '../../base/version' import type { VersionProps } from '../../../types' +import usePluginInstallLimit from '../../hooks/use-install-plugin-limit' type Props = { checked: boolean @@ -29,9 +30,11 @@ const LoadedItem: FC<Props> = ({ ...particleVersionInfo, toInstallVersion: payload.version, } + const { canInstall } = usePluginInstallLimit(payload) return ( <div className='flex items-center space-x-2'> <Checkbox + disabled={!canInstall} className='shrink-0' checked={checked} onCheck={() => onCheckedChange(payload)} @@ -43,6 +46,7 @@ const LoadedItem: FC<Props> = ({ icon: isFromMarketPlace ? `${MARKETPLACE_API_PREFIX}/plugins/${payload.org}/${payload.name}/icon` : getIconUrl(payload.icon), }} titleLeft={payload.version ? <Version {...versionInfo} /> : null} + limitedInstall={!canInstall} /> </div> ) diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx index 101c8facaf..eac03011ad 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx @@ -29,7 +29,7 @@ const PackageItem: FC<Props> = ({ const plugin = pluginManifestToCardPluginProps(payload.value.manifest) return ( <LoadedItem - payload={plugin} + payload={{ ...plugin, from: payload.type }} checked={checked} onCheckedChange={onCheckedChange} isFromMarketPlace={isFromMarketPlace} diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx index 5f60483d3c..ee45520a61 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx @@ -1,32 +1,56 @@ 'use client' -import type { FC } from 'react' +import type { ForwardRefRenderFunction } from 'react' +import { useImperativeHandle } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react' import type { Dependency, GitHubItemAndMarketPlaceDependency, PackageDependency, Plugin, VersionInfo } from '../../../types' import MarketplaceItem from '../item/marketplace-item' import GithubItem from '../item/github-item' -import { useFetchPluginsInMarketPlaceByIds, useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' +import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed' import produce from 'immer' import PackageItem from '../item/package-item' import LoadingError from '../../base/loading-error' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit' type Props = { allPlugins: Dependency[] selectedPlugins: Plugin[] - onSelect: (plugin: Plugin, selectedIndex: number) => void + onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void + onSelectAll: (plugins: Plugin[], selectedIndexes: number[]) => void + onDeSelectAll: () => void onLoadedAllPlugin: (installedInfo: Record<string, VersionInfo>) => void isFromMarketPlace?: boolean } -const InstallByDSLList: FC<Props> = ({ +export type ExposeRefs = { + selectAllPlugins: () => void + deSelectAllPlugins: () => void +} + +const InstallByDSLList: ForwardRefRenderFunction<ExposeRefs, Props> = ({ allPlugins, selectedPlugins, onSelect, + onSelectAll, + onDeSelectAll, onLoadedAllPlugin, isFromMarketPlace, -}) => { +}, ref) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) // DSL has id, to get plugin info to show more info - const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByIds(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value.marketplace_plugin_unique_identifier!)) + const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => { + const dependecy = (d as GitHubItemAndMarketPlaceDependency).value + // split org, name, version by / and : + // and remove @ and its suffix + const [orgPart, nameAndVersionPart] = dependecy.marketplace_plugin_unique_identifier!.split('@')[0].split('/') + const [name, version] = nameAndVersionPart.split(':') + return { + organization: orgPart, + plugin: name, + version, + } + })) // has meta(org,name,version), to get id const { isLoading: isFetchingDataByMeta, data: infoByMeta, error: infoByMetaError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map(d => (d as GitHubItemAndMarketPlaceDependency).value!)) @@ -82,11 +106,12 @@ const InstallByDSLList: FC<Props> = ({ }, [allPlugins]) useEffect(() => { - if (!isFetchingMarketplaceDataById && infoGetById?.data.plugins) { + if (!isFetchingMarketplaceDataById && infoGetById?.data.list) { const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => { const p = d as GitHubItemAndMarketPlaceDependency const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0] - return infoGetById.data.plugins.find(item => item.plugin_id === id)! + const retPluginInfo = infoGetById.data.list.find(item => item.plugin.plugin_id === id)?.plugin + return { ...retPluginInfo, from: d.type } as Plugin }) const payloads = sortedList const failedIndex: number[] = [] @@ -95,7 +120,7 @@ const InstallByDSLList: FC<Props> = ({ if (payloads[i]) { draft[index] = { ...payloads[i], - version: payloads[i].version || payloads[i].latest_version, + version: payloads[i]!.version || payloads[i]!.latest_version, } } else { failedIndex.push(index) } @@ -170,9 +195,35 @@ const InstallByDSLList: FC<Props> = ({ const handleSelect = useCallback((index: number) => { return () => { - onSelect(plugins[index]!, index) + const canSelectPlugins = plugins.filter((p) => { + const { canInstall } = pluginInstallLimit(p!, systemFeatures) + return canInstall + }) + onSelect(plugins[index]!, index, canSelectPlugins.length) } - }, [onSelect, plugins]) + }, [onSelect, plugins, systemFeatures]) + + useImperativeHandle(ref, () => ({ + selectAllPlugins: () => { + const selectedIndexes: number[] = [] + const selectedPlugins: Plugin[] = [] + allPlugins.forEach((d, index) => { + const p = plugins[index] + if (!p) + return + const { canInstall } = pluginInstallLimit(p, systemFeatures) + if (canInstall) { + selectedIndexes.push(index) + selectedPlugins.push(p) + } + }) + onSelectAll(selectedPlugins, selectedIndexes) + }, + deSelectAllPlugins: () => { + onDeSelectAll() + }, + })) + return ( <> {allPlugins.map((d, index) => { @@ -200,7 +251,7 @@ const InstallByDSLList: FC<Props> = ({ key={index} checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)} onCheckedChange={handleSelect(index)} - payload={plugin} + payload={{ ...plugin, from: d.type } as Plugin} version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''} versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)} /> @@ -223,4 +274,4 @@ const InstallByDSLList: FC<Props> = ({ </> ) } -export default React.memo(InstallByDSLList) +export default React.forwardRef(InstallByDSLList) diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx index db24bdd97a..2d8bdcd3d9 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx @@ -1,15 +1,18 @@ 'use client' import type { FC } from 'react' +import { useRef } from 'react' import React, { useCallback, useState } from 'react' import type { Dependency, InstallStatusResponse, Plugin, VersionInfo } from '../../../types' import Button from '@/app/components/base/button' import { RiLoader2Line } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import type { ExposeRefs } from './install-multi' import InstallMulti from './install-multi' import { useInstallOrUpdate } from '@/service/use-plugins' import useRefreshPluginList from '../../hooks/use-refresh-plugin-list' import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission' import { useMittContextSelector } from '@/context/mitt-context' +import Checkbox from '@/app/components/base/checkbox' const i18nPrefix = 'plugin.installModal' type Props = { @@ -34,18 +37,8 @@ const Install: FC<Props> = ({ const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([]) const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([]) const selectedPluginsNum = selectedPlugins.length + const installMultiRef = useRef<ExposeRefs>(null) const { refreshPluginList } = useRefreshPluginList() - const handleSelect = (plugin: Plugin, selectedIndex: number) => { - const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) - let nextSelectedPlugins - if (isSelected) - nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) - else - nextSelectedPlugins = [...selectedPlugins, plugin] - setSelectedPlugins(nextSelectedPlugins) - const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] - setSelectedIndexes(nextSelectedIndexes) - } const [canInstall, setCanInstall] = React.useState(false) const [installedInfo, setInstalledInfo] = useState<Record<string, VersionInfo> | undefined>(undefined) @@ -81,6 +74,51 @@ const Install: FC<Props> = ({ installedInfo: installedInfo!, }) } + const [isSelectAll, setIsSelectAll] = useState(false) + const [isIndeterminate, setIsIndeterminate] = useState(false) + const handleClickSelectAll = useCallback(() => { + if (isSelectAll) + installMultiRef.current?.deSelectAllPlugins() + else + installMultiRef.current?.selectAllPlugins() + }, [isSelectAll]) + const handleSelectAll = useCallback((plugins: Plugin[], selectedIndexes: number[]) => { + setSelectedPlugins(plugins) + setSelectedIndexes(selectedIndexes) + setIsSelectAll(true) + setIsIndeterminate(false) + }, []) + const handleDeSelectAll = useCallback(() => { + setSelectedPlugins([]) + setSelectedIndexes([]) + setIsSelectAll(false) + setIsIndeterminate(false) + }, []) + + const handleSelect = useCallback((plugin: Plugin, selectedIndex: number, allPluginsLength: number) => { + const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id) + let nextSelectedPlugins + if (isSelected) + nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id) + else + nextSelectedPlugins = [...selectedPlugins, plugin] + setSelectedPlugins(nextSelectedPlugins) + const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex] + setSelectedIndexes(nextSelectedIndexes) + if (nextSelectedPlugins.length === 0) { + setIsSelectAll(false) + setIsIndeterminate(false) + } + else if (nextSelectedPlugins.length === allPluginsLength) { + setIsSelectAll(true) + setIsIndeterminate(false) + } + else { + setIsIndeterminate(true) + setIsSelectAll(false) + } + }, [selectedPlugins, selectedIndexes]) + const { canInstallPluginFromMarketplace } = useCanInstallPluginFromMarketplace() return ( <> @@ -90,9 +128,12 @@ const Install: FC<Props> = ({ </div> <div className='w-full space-y-1 rounded-2xl bg-background-section-burn p-2'> <InstallMulti + ref={installMultiRef} allPlugins={allPlugins} selectedPlugins={selectedPlugins} onSelect={handleSelect} + onSelectAll={handleSelectAll} + onDeSelectAll={handleDeSelectAll} onLoadedAllPlugin={handleLoadedAllPlugin} isFromMarketPlace={isFromMarketPlace} /> @@ -100,21 +141,29 @@ const Install: FC<Props> = ({ </div> {/* Action Buttons */} {!isHideButton && ( - <div className='flex items-center justify-end gap-2 self-stretch p-6 pt-5'> - {!canInstall && ( - <Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> - {t('common.operation.cancel')} + <div className='flex items-center justify-between gap-2 self-stretch p-6 pt-5'> + <div className='px-2'> + {canInstall && <div className='flex items-center gap-x-2' onClick={handleClickSelectAll}> + <Checkbox checked={isSelectAll} indeterminate={isIndeterminate} /> + <p className='system-sm-medium cursor-pointer text-text-secondary'>{isSelectAll ? t('common.operation.deSelectAll') : t('common.operation.selectAll')}</p> + </div>} + </div> + <div className='flex items-center justify-end gap-2 self-stretch'> + {!canInstall && ( + <Button variant='secondary' className='min-w-[72px]' onClick={onCancel}> + {t('common.operation.cancel')} + </Button> + )} + <Button + variant='primary' + className='flex min-w-[72px] space-x-0.5' + disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace} + onClick={handleInstall} + > + {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} + <span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span> </Button> - )} - <Button - variant='primary' - className='flex min-w-[72px] space-x-0.5' - disabled={!canInstall || isInstalling || selectedPlugins.length === 0 || !canInstallPluginFromMarketplace} - onClick={handleInstall} - > - {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} - <span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span> - </Button> + </div> </div> )} diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx index 24e1e39ff5..c77d0d0a70 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx @@ -83,6 +83,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({ installedValue={updatePayload?.originalPackageInfo.version} placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''} popupClassName='w-[512px] z-[1001]' + triggerClassName='text-components-input-text-filled' /> <label htmlFor='package' @@ -97,6 +98,7 @@ const SelectPackage: React.FC<SelectPackageProps> = ({ readonly={!selectedVersion} placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''} popupClassName='w-[512px] z-[1001]' + triggerClassName='text-components-input-text-filled' /> <div className='mt-4 flex items-center justify-end gap-2 self-stretch'> {!isEdit diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 412517283c..1ddc52ced9 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -124,7 +124,7 @@ const Installed: FC<Props> = ({ /> </p> {!isDifyVersionCompatible && ( - <p className='system-md-regular flex items-center gap-1 text-text-secondary text-text-warning'> + <p className='system-md-regular flex items-center gap-1 text-text-warning'> {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: payload.meta.minimum_dify_version })} </p> )} diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index 53f1f40fb8..dbc7c97d88 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -15,6 +15,7 @@ import Version from '../../base/version' import { usePluginTaskList } from '@/service/use-plugins' import { gte } from 'semver' import { useAppContext } from '@/context/app-context' +import useInstallPluginLimit from '../../hooks/use-install-plugin-limit' const i18nPrefix = 'plugin.installModal' @@ -124,15 +125,16 @@ const Installed: FC<Props> = ({ const isDifyVersionCompatible = useMemo(() => { if (!pluginDeclaration || !langeniusVersionInfo.current_version) return true return gte(langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0') - }, [langeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version]) + }, [langeniusVersionInfo.current_version, pluginDeclaration]) + const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' }) return ( <> <div className='flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3'> <div className='system-md-regular text-text-secondary'> <p>{t(`${i18nPrefix}.readyToInstall`)}</p> {!isDifyVersionCompatible && ( - <p className='system-md-regular text-text-secondary text-text-warning'> + <p className='system-md-regular text-text-warning'> {t('plugin.difyVersionNotCompatible', { minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })} </p> )} @@ -146,6 +148,7 @@ const Installed: FC<Props> = ({ installedVersion={installedVersion} toInstallVersion={toInstallVersion} />} + limitedInstall={!canInstall} /> </div> </div> @@ -159,7 +162,7 @@ const Installed: FC<Props> = ({ <Button variant='primary' className='flex min-w-[72px] space-x-0.5' - disabled={isInstalling || isLoading} + disabled={isInstalling || isLoading || !canInstall} onClick={handleInstall} > {isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />} diff --git a/web/app/components/plugins/install-plugin/utils.ts b/web/app/components/plugins/install-plugin/utils.ts index eff5e3a6d5..43df105026 100644 --- a/web/app/components/plugins/install-plugin/utils.ts +++ b/web/app/components/plugins/install-plugin/utils.ts @@ -1,5 +1,6 @@ import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' import type { GitHubUrlInfo } from '@/app/components/plugins/types' +import { isEmpty } from 'lodash-es' export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => { return { @@ -47,6 +48,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife }, tags: [], badges: pluginManifest.badges, + verification: isEmpty(pluginManifest.verification) ? { authorized_category: 'langgenius' } : pluginManifest.verification, } } diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 27511559d9..8d9f3a5b10 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -1,7 +1,7 @@ 'use client' import { useTheme } from 'next-themes' import { RiArrowRightUpLine } from '@remixicon/react' -import { getPluginLinkInMarketplace } from '../utils' +import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import type { Plugin } from '@/app/components/plugins/types' @@ -56,7 +56,7 @@ const CardWrapper = ({ > {t('plugin.detailPanel.operation.install')} </Button> - <a href={`${getPluginLinkInMarketplace(plugin)}?language=${localeFromLocale}${theme ? `&theme=${theme}` : ''}`} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'> + <a href={getPluginLinkInMarketplace(plugin, { language: localeFromLocale, theme })} target='_blank' className='block w-[calc(50%-4px)] flex-1 shrink-0'> <Button className='w-full gap-0.5' > @@ -83,7 +83,7 @@ const CardWrapper = ({ return ( <a className='group relative inline-block cursor-pointer rounded-xl' - href={getPluginLinkInMarketplace(plugin)} + href={getPluginDetailLinkInMarketplace(plugin)} > <Card key={plugin.name} diff --git a/web/app/components/plugins/marketplace/search-box/index.tsx b/web/app/components/plugins/marketplace/search-box/index.tsx index 217007846c..5f19afbba6 100644 --- a/web/app/components/plugins/marketplace/search-box/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/index.tsx @@ -1,8 +1,9 @@ 'use client' -import { RiCloseLine } from '@remixicon/react' +import { RiCloseLine, RiSearchLine } from '@remixicon/react' import TagsFilter from './tags-filter' import ActionButton from '@/app/components/base/action-button' import cn from '@/utils/classnames' +import { RiAddLine } from '@remixicon/react' type SearchBoxProps = { search: string @@ -13,6 +14,9 @@ type SearchBoxProps = { size?: 'small' | 'large' placeholder?: string locale?: string + supportAddCustomTool?: boolean + onShowAddCustomCollectionModal?: () => void + onAddedCustomTool?: () => void } const SearchBox = ({ search, @@ -23,46 +27,62 @@ const SearchBox = ({ size = 'small', placeholder = '', locale, + supportAddCustomTool, + onShowAddCustomCollectionModal, }: SearchBoxProps) => { return ( <div - className={cn( - 'z-[11] flex items-center', - size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', - size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5', - inputClassName, - )} + className='z-[11] flex items-center' > - <TagsFilter - tags={tags} - onTagsChange={onTagsChange} - size={size} - locale={locale} - /> - <div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div> - <div className='relative flex grow items-center p-1 pl-2'> - <div className='mr-2 flex w-full items-center'> - <input - className={cn( - 'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none', - )} - value={search} - onChange={(e) => { - onSearchChange(e.target.value) - }} - placeholder={placeholder} - /> - { - search && ( - <div className='absolute right-2 top-1/2 -translate-y-1/2'> - <ActionButton onClick={() => onSearchChange('')}> - <RiCloseLine className='h-4 w-4' /> - </ActionButton> - </div> - ) - } + <div className={ + cn('flex items-center', + size === 'large' && 'rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1.5 shadow-md', + size === 'small' && 'rounded-lg bg-components-input-bg-normal p-0.5', + inputClassName, + ) + }> + <div className='relative flex grow items-center p-1 pl-2'> + <div className='mr-2 flex w-full items-center'> + <RiSearchLine className='mr-1.5 size-4 text-text-placeholder' /> + <input + className={cn( + 'body-md-medium block grow appearance-none bg-transparent text-text-secondary outline-none', + )} + value={search} + onChange={(e) => { + onSearchChange(e.target.value) + }} + placeholder={placeholder} + /> + { + search && ( + <div className='absolute right-2 top-1/2 -translate-y-1/2'> + <ActionButton onClick={() => onSearchChange('')}> + <RiCloseLine className='h-4 w-4' /> + </ActionButton> + </div> + ) + } + </div> </div> + <div className='mx-1 h-3.5 w-[1px] bg-divider-regular'></div> + <TagsFilter + tags={tags} + onTagsChange={onTagsChange} + size={size} + locale={locale} + /> </div> + {supportAddCustomTool && ( + <div className='flex shrink-0 items-center'> + <ActionButton + className='ml-2 rounded-full bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text' + onClick={onShowAddCustomCollectionModal} + > + <RiAddLine className='h-4 w-4' /> + </ActionButton> + </div> + )} </div> ) } diff --git a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx index edf50dc874..bae6491727 100644 --- a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx +++ b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx @@ -2,9 +2,7 @@ import { useState } from 'react' import { - RiArrowDownSLine, - RiCloseCircleFill, - RiFilter3Line, + RiPriceTag3Line, } from '@remixicon/react' import { PortalToFollowElem, @@ -57,47 +55,15 @@ const TagsFilter = ({ onClick={() => setOpen(v => !v)} > <div className={cn( - 'flex cursor-pointer items-center rounded-lg text-text-tertiary hover:bg-state-base-hover', - size === 'large' && 'h-8 px-2 py-1', - size === 'small' && 'h-7 py-0.5 pl-1 pr-1.5 ', - selectedTagsLength && 'text-text-secondary', - open && 'bg-state-base-hover', + 'ml-0.5 mr-1.5 flex items-center text-text-tertiary ', + size === 'large' && 'h-8 py-1', + size === 'small' && 'h-7 py-0.5 ', + // selectedTagsLength && 'text-text-secondary', + // open && 'bg-state-base-hover', )}> - <div className='p-0.5'> - <RiFilter3Line className='h-4 w-4' /> + <div className='cursor-pointer rounded-md p-0.5 hover:bg-state-base-hover'> + <RiPriceTag3Line className='h-4 w-4 text-text-tertiary' /> </div> - <div className={cn( - 'system-sm-medium flex items-center p-1', - size === 'large' && 'p-1', - size === 'small' && 'px-0.5 py-1', - )}> - { - !selectedTagsLength && t('pluginTags.allTags') - } - { - !!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',') - } - { - selectedTagsLength > 2 && ( - <div className='system-xs-medium ml-1 text-text-tertiary'> - +{selectedTagsLength - 2} - </div> - ) - } - </div> - { - !!selectedTagsLength && ( - <RiCloseCircleFill - className='h-4 w-4 cursor-pointer text-text-quaternary' - onClick={() => onTagsChange([])} - /> - ) - } - { - !selectedTagsLength && ( - <RiArrowDownSLine className='h-4 w-4' /> - ) - } </div> </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[1000]'> diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index ef4822f849..b3032968ab 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -8,8 +8,8 @@ import type { } from '@/app/components/plugins/marketplace/types' import { MARKETPLACE_API_PREFIX, - MARKETPLACE_URL_PREFIX, } from '@/config' +import { getMarketplaceUrl } from '@/utils/var' export const getPluginIconInMarketplace = (plugin: Plugin) => { if (plugin.type === 'bundle') @@ -32,10 +32,16 @@ export const getFormattedPlugin = (bundle: any) => { } } -export const getPluginLinkInMarketplace = (plugin: Plugin) => { +export const getPluginLinkInMarketplace = (plugin: Plugin, params?: Record<string, string | undefined>) => { if (plugin.type === 'bundle') - return `${MARKETPLACE_URL_PREFIX}/bundles/${plugin.org}/${plugin.name}` - return `${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}` + return getMarketplaceUrl(`/bundles/${plugin.org}/${plugin.name}`, params) + return getMarketplaceUrl(`/plugins/${plugin.org}/${plugin.name}`, params) +} + +export const getPluginDetailLinkInMarketplace = (plugin: Plugin) => { + if (plugin.type === 'bundle') + return `/bundles/${plugin.org}/${plugin.name}` + return `/plugins/${plugin.org}/${plugin.name}` } export const getMarketplacePluginsByCollectionId = async (collectionId: string, query?: CollectionsAndPluginsSearchParams) => { diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx index b22f59fe2c..d3ac9d7d2e 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx @@ -62,7 +62,7 @@ const AppInputsPanel = ({ return [] let inputFormSchema = [] if (isBasicApp) { - inputFormSchema = currentApp.model_config.user_input_form.filter((item: any) => !item.external_data_tool).map((item: any) => { + inputFormSchema = currentApp.model_config?.user_input_form?.filter((item: any) => !item.external_data_tool).map((item: any) => { if (item.paragraph) { return { ...item.paragraph, @@ -108,10 +108,10 @@ const AppInputsPanel = ({ type: 'text-input', required: false, } - }) + }) || [] } else { - const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any + const startNode = currentWorkflow?.graph?.nodes.find(node => node.data.type === BlockEnum.Start) as any inputFormSchema = startNode?.data.variables.map((variable: any) => { if (variable.type === InputVarType.multiFiles) { return { @@ -132,7 +132,7 @@ const AppInputsPanel = ({ ...variable, required: false, } - }) + }) || [] } if ((currentApp.mode === 'completion' || currentApp.mode === 'workflow') && basicAppFileConfig.enabled) { inputFormSchema.push({ @@ -144,7 +144,7 @@ const AppInputsPanel = ({ fileUploadConfig, }) } - return inputFormSchema + return inputFormSchema || [] }, [basicAppFileConfig, currentApp, currentWorkflow, fileUploadConfig, isBasicApp]) const handleFormChange = (value: Record<string, any>) => { diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx index 2f57276559..3c79acb653 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useMemo } from 'react' -import { useState } from 'react' +import React, { useCallback, useEffect, useRef } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, @@ -14,9 +13,9 @@ import type { import Input from '@/app/components/base/input' import AppIcon from '@/app/components/base/app-icon' import type { App } from '@/types/app' +import { useTranslation } from 'react-i18next' type Props = { - appList: App[] scope: string disabled: boolean trigger: React.ReactNode @@ -25,11 +24,16 @@ type Props = { isShow: boolean onShowChange: (isShow: boolean) => void onSelect: (app: App) => void + apps: App[] + isLoading: boolean + hasMore: boolean + onLoadMore: () => void + searchText: string + onSearchChange: (text: string) => void } const AppPicker: FC<Props> = ({ scope, - appList, disabled, trigger, placement = 'right-start', @@ -37,19 +41,81 @@ const AppPicker: FC<Props> = ({ isShow, onShowChange, onSelect, + apps, + isLoading, + hasMore, + onLoadMore, + searchText, + onSearchChange, }) => { - const [searchText, setSearchText] = useState('') - const filteredAppList = useMemo(() => { - return (appList || []) - .filter(app => app.name.toLowerCase().includes(searchText.toLowerCase())) - .filter(app => (app.mode !== 'advanced-chat' && app.mode !== 'workflow') || !!app.workflow) - .filter(app => scope === 'all' - || (scope === 'completion' && app.mode === 'completion') - || (scope === 'workflow' && app.mode === 'workflow') - || (scope === 'chat' && app.mode === 'advanced-chat') - || (scope === 'chat' && app.mode === 'agent-chat') - || (scope === 'chat' && app.mode === 'chat')) - }, [appList, scope, searchText]) + const { t } = useTranslation() + const observerTarget = useRef<HTMLDivElement>(null) + const observerRef = useRef<IntersectionObserver | null>(null) + const loadingRef = useRef(false) + + const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => { + const target = entries[0] + if (!target.isIntersecting || loadingRef.current || !hasMore || isLoading) return + + loadingRef.current = true + onLoadMore() + // Reset loading state + setTimeout(() => { + loadingRef.current = false + }, 500) + }, [hasMore, isLoading, onLoadMore]) + + useEffect(() => { + if (!isShow) { + if (observerRef.current) { + observerRef.current.disconnect() + observerRef.current = null + } + return + } + + let mutationObserver: MutationObserver | null = null + + const setupIntersectionObserver = () => { + if (!observerTarget.current) return + + // Create new observer + observerRef.current = new IntersectionObserver(handleIntersection, { + root: null, + rootMargin: '100px', + threshold: 0.1, + }) + + observerRef.current.observe(observerTarget.current) + } + + // Set up MutationObserver to watch DOM changes + mutationObserver = new MutationObserver((mutations) => { + if (observerTarget.current) { + setupIntersectionObserver() + mutationObserver?.disconnect() + } + }) + + // Watch body changes since Portal adds content to body + mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }) + + // If element exists, set up IntersectionObserver directly + if (observerTarget.current) + setupIntersectionObserver() + + return () => { + if (observerRef.current) { + observerRef.current.disconnect() + observerRef.current = null + } + mutationObserver?.disconnect() + } + }, [isShow, handleIntersection]) + const getAppType = (app: App) => { switch (app.mode) { case 'advanced-chat': @@ -84,18 +150,18 @@ const AppPicker: FC<Props> = ({ </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[1000]'> - <div className="relative min-h-20 w-[356px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm"> + <div className="relative flex max-h-[400px] min-h-20 w-[356px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm"> <div className='p-2 pb-1'> <Input showLeftIcon showClearIcon value={searchText} - onChange={e => setSearchText(e.target.value)} - onClear={() => setSearchText('')} + onChange={e => onSearchChange(e.target.value)} + onClear={() => onSearchChange('')} /> </div> - <div className='p-1'> - {filteredAppList.map(app => ( + <div className='min-h-0 flex-1 overflow-y-auto p-1'> + {apps.map(app => ( <div key={app.id} className='flex cursor-pointer items-center gap-3 rounded-lg py-1 pl-2 pr-3 hover:bg-state-base-hover' @@ -113,6 +179,13 @@ const AppPicker: FC<Props> = ({ <div className='system-2xs-medium-uppercase shrink-0 text-text-tertiary'>{getAppType(app)}</div> </div> ))} + <div ref={observerTarget} className='h-4 w-full'> + {isLoading && ( + <div className='flex justify-center py-2'> + <div className='text-sm text-gray-500'>{t('common.loading')}</div> + </div> + )} + </div> </div> </div> </PortalToFollowElemContent> diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx index 39ca957953..9c16c66a70 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { PortalToFollowElem, @@ -10,12 +10,36 @@ import { import AppTrigger from '@/app/components/plugins/plugin-detail-panel/app-selector/app-trigger' import AppPicker from '@/app/components/plugins/plugin-detail-panel/app-selector/app-picker' import AppInputsPanel from '@/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel' -import { useAppFullList } from '@/service/use-apps' import type { App } from '@/types/app' import type { OffsetOptions, Placement, } from '@floating-ui/react' +import useSWRInfinite from 'swr/infinite' +import { fetchAppList } from '@/service/apps' +import type { AppListResponse } from '@/models/app' + +const PAGE_SIZE = 20 + +const getKey = ( + pageIndex: number, + previousPageData: AppListResponse, + searchText: string, +) => { + if (pageIndex === 0 || (previousPageData && previousPageData.has_more)) { + const params: any = { + url: 'apps', + params: { + page: pageIndex + 1, + limit: PAGE_SIZE, + name: searchText, + }, + } + + return params + } + return null +} type Props = { value?: { @@ -34,6 +58,7 @@ type Props = { }) => void supportAddCustomTool?: boolean } + const AppSelector: FC<Props> = ({ value, scope, @@ -44,18 +69,47 @@ const AppSelector: FC<Props> = ({ }) => { const { t } = useTranslation() const [isShow, onShowChange] = useState(false) + const [searchText, setSearchText] = useState('') + const [isLoadingMore, setIsLoadingMore] = useState(false) + + const { data, isLoading, setSize } = useSWRInfinite( + (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, searchText), + fetchAppList, + { + revalidateFirstPage: true, + shouldRetryOnError: false, + dedupingInterval: 500, + errorRetryCount: 3, + }, + ) + + const displayedApps = useMemo(() => { + if (!data) return [] + return data.flatMap(({ data: apps }) => apps) + }, [data]) + + const hasMore = data?.at(-1)?.has_more ?? true + + const handleLoadMore = useCallback(async () => { + if (isLoadingMore || !hasMore) return + + setIsLoadingMore(true) + try { + await setSize((size: number) => size + 1) + } + finally { + // Add a small delay to ensure state updates are complete + setTimeout(() => { + setIsLoadingMore(false) + }, 300) + } + }, [isLoadingMore, hasMore, setSize]) + const handleTriggerClick = () => { if (disabled) return onShowChange(true) } - const { data: appList } = useAppFullList() - const currentAppInfo = useMemo(() => { - if (!appList?.data || !value) - return undefined - return appList.data.find(app => app.id === value.app_id) - }, [appList?.data, value]) - const [isShowChooseApp, setIsShowChooseApp] = useState(false) const handleSelectApp = (app: App) => { const clearValue = app.id !== value?.app_id @@ -67,6 +121,7 @@ const AppSelector: FC<Props> = ({ onSelect(appValue) setIsShowChooseApp(false) } + const handleFormChange = (inputs: Record<string, any>) => { const newFiles = inputs['#image#'] delete inputs['#image#'] @@ -88,6 +143,12 @@ const AppSelector: FC<Props> = ({ } }, [value]) + const currentAppInfo = useMemo(() => { + if (!displayedApps || !value) + return undefined + return displayedApps.find(app => app.id === value.app_id) + }, [displayedApps, value]) + return ( <> <PortalToFollowElem @@ -121,9 +182,14 @@ const AppSelector: FC<Props> = ({ isShow={isShowChooseApp} onShowChange={setIsShowChooseApp} disabled={false} - appList={appList?.data || []} onSelect={handleSelectApp} scope={scope || 'all'} + apps={displayedApps} + isLoading={isLoading || isLoadingMore} + hasMore={hasMore} + onLoadMore={handleLoadMore} + searchText={searchText} + onSearchChange={setSearchText} /> </div> {/* app inputs config panel */} @@ -140,4 +206,5 @@ const AppSelector: FC<Props> = ({ </> ) } + export default React.memo(AppSelector) diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 7548c90ac5..0a5a8b87d6 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -33,8 +33,9 @@ import { useGetLanguage } from '@/context/i18n' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { useInvalidateAllToolProviders } from '@/service/use-tools' -import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' +import { API_PREFIX } from '@/config' import cn from '@/utils/classnames' +import { getMarketplaceUrl } from '@/utils/var' const i18nPrefix = 'plugin.action' @@ -87,7 +88,7 @@ const DetailHeader = ({ if (isFromGitHub) return `https://github.com/${meta!.repo}` if (isFromMarketplace) - return `${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}` + return getMarketplaceUrl(`/plugins/${author}/${name}`, { theme }) return '' }, [author, isFromGitHub, isFromMarketplace, meta, name, theme]) diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 59a457ecbd..5735022c5d 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useDocLink } from '@/context/i18n' import { useBoolean } from 'ahooks' import { RiAddLine, @@ -20,8 +20,6 @@ import { useInvalidateEndpointList, } from '@/service/use-endpoints' import type { PluginDetail } from '@/app/components/plugins/types' -import { LanguagesSupported } from '@/i18n/language' -import I18n from '@/context/i18n' import cn from '@/utils/classnames' type Props = { @@ -29,7 +27,7 @@ type Props = { } const EndpointList = ({ detail }: Props) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const pluginUniqueID = detail.plugin_unique_identifier const declaration = detail.declaration.endpoint const showTopBorder = detail.declaration.tool @@ -70,7 +68,6 @@ const EndpointList = ({ detail }: Props) => { {t('plugin.detailPanel.endpoints')} <Tooltip position='right' - needsDelay popupClassName='w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border' popupContent={ <div className='flex flex-col gap-2'> @@ -79,7 +76,7 @@ const EndpointList = ({ detail }: Props) => { </div> <div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.endpointsTip')}</div> <a - href={`https://docs.dify.ai/${locale === LanguagesSupported[1] ? 'v/zh-hans/' : ''}plugins/schema-definition/endpoint`} + href={docLink('/plugins/schema-definition/endpoint')} target='_blank' rel='noopener noreferrer' > diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index fd862720af..130773e0c2 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -18,6 +18,15 @@ type Props = { onSaved: (value: Record<string, any>) => void } +const extractDefaultValues = (schemas: any[]) => { + const result: Record<string, any> = {} + for (const field of schemas) { + if (field.default !== undefined) + result[field.name] = field.default + } + return result +} + const EndpointModal: FC<Props> = ({ formSchemas, defaultValues = {}, @@ -26,7 +35,10 @@ const EndpointModal: FC<Props> = ({ }) => { const getValueFromI18nObject = useRenderI18nObject() const { t } = useTranslation() - const [tempCredential, setTempCredential] = React.useState<any>(defaultValues) + const initialValues = Object.keys(defaultValues).length > 0 + ? defaultValues + : extractDefaultValues(formSchemas) + const [tempCredential, setTempCredential] = React.useState<any>(initialValues) const handleSave = () => { for (const field of formSchemas) { diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx index 60c63db5ea..51702a780a 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx @@ -25,6 +25,8 @@ import LLMParamsPanel from './llm-params-panel' import TTSParamsPanel from './tts-params-panel' import { useProviderContext } from '@/context/provider-context' import cn from '@/utils/classnames' +import Toast from '@/app/components/base/toast' +import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' export type ModelParameterModalProps = { popupClassName?: string @@ -121,17 +123,42 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({ return !isAPIKeySet || hasDeprecated || modelDisabled }, [hasDeprecated, isAPIKeySet, modelDisabled]) - const handleChangeModel = ({ provider, model }: DefaultModel) => { + const handleChangeModel = async ({ provider, model }: DefaultModel) => { const targetProvider = scopedModelList.find(modelItem => modelItem.provider === provider) const targetModelItem = targetProvider?.models.find((modelItem: { model: string }) => modelItem.model === model) const model_type = targetModelItem?.model_type as string + + let nextCompletionParams: FormValue = {} + + if (model_type === ModelTypeEnum.textGeneration) { + try { + const { params: filtered, removedDetails } = await fetchAndMergeValidCompletionParams( + provider, + model, + value?.completion_params, + ) + nextCompletionParams = filtered + + const keys = Object.keys(removedDetails || {}) + if (keys.length) { + Toast.notify({ + type: 'warning', + message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`, + }) + } + } + catch (e) { + Toast.notify({ type: 'error', message: t('common.error') }) + } + } + setModel({ provider, model, model_type, ...(model_type === ModelTypeEnum.textGeneration ? { mode: targetModelItem?.model_properties.mode as string, - completion_params: {}, + completion_params: nextCompletionParams, } : {}), }) } diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index 7f5f22896a..44212e38ed 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -13,6 +13,7 @@ import type { Node } from 'reactflow' import type { NodeOutPutVar } from '@/app/components/workflow/types' import cn from '@/utils/classnames' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general' +import { useAllMCPTools } from '@/service/use-tools' type Props = { disabled?: boolean @@ -26,6 +27,7 @@ type Props = { nodeOutputVars: NodeOutPutVar[], availableNodes: Node[], nodeId?: string + canChooseMCPTool?: boolean } const MultipleToolSelector = ({ @@ -40,9 +42,16 @@ const MultipleToolSelector = ({ nodeOutputVars, availableNodes, nodeId, + canChooseMCPTool, }: Props) => { const { t } = useTranslation() - const enabledCount = value.filter(item => item.enabled).length + const { data: mcpTools } = useAllMCPTools() + const enabledCount = value.filter((item) => { + const isMCPTool = mcpTools?.find(tool => tool.id === item.provider_name) + if(isMCPTool) + return item.enabled && canChooseMCPTool + return item.enabled + }).length // collapse control const [collapse, setCollapse] = React.useState(false) const handleCollapse = () => { @@ -66,6 +75,19 @@ const MultipleToolSelector = ({ setOpen(false) } + const handleAddMultiple = (val: ToolValue[]) => { + const newValue = [...value, ...val] + // deduplication + const deduplication = newValue.reduce((acc, cur) => { + if (!acc.find(item => item.provider_name === cur.provider_name && item.tool_name === cur.tool_name)) + acc.push(cur) + return acc + }, [] as ToolValue[]) + // update value + onChange(deduplication) + setOpen(false) + } + // delete tool const handleDelete = (index: number) => { const newValue = [...value] @@ -92,7 +114,6 @@ const MultipleToolSelector = ({ {tooltip && ( <Tooltip popupContent={tooltip} - needsDelay > <div><RiQuestionLine className='h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary' /></div> </Tooltip> @@ -117,6 +138,7 @@ const MultipleToolSelector = ({ )} {!disabled && ( <ActionButton className='mx-1' onClick={() => { + setCollapse(false) setOpen(!open) setPanelShowState(true) }}> @@ -126,23 +148,6 @@ const MultipleToolSelector = ({ </div> {!collapse && ( <> - <ToolSelector - nodeId={nodeId} - nodeOutputVars={nodeOutputVars} - availableNodes={availableNodes} - scope={scope} - value={undefined} - selectedTools={value} - onSelect={handleAdd} - controlledState={open} - onControlledStateChange={setOpen} - trigger={ - <div className=''></div> - } - panelShowState={panelShowState} - onPanelShowStateChange={setPanelShowState} - isEdit={false} - /> {value.length === 0 && ( <div className='system-xs-regular flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t('plugin.detailPanel.toolSelector.empty')}</div> )} @@ -156,14 +161,35 @@ const MultipleToolSelector = ({ value={item} selectedTools={value} onSelect={item => handleConfigure(item, index)} + onSelectMultiple={handleAddMultiple} onDelete={() => handleDelete(index)} supportEnableSwitch + canChooseMCPTool={canChooseMCPTool} isEdit /> </div> ))} </> )} + <ToolSelector + nodeId={nodeId} + nodeOutputVars={nodeOutputVars} + availableNodes={availableNodes} + scope={scope} + value={undefined} + selectedTools={value} + onSelect={handleAdd} + controlledState={open} + onControlledStateChange={setOpen} + trigger={ + <div className=''></div> + } + panelShowState={panelShowState} + onPanelShowStateChange={setPanelShowState} + isEdit={false} + canChooseMCPTool={canChooseMCPTool} + onSelectMultiple={handleAddMultiple} + /> </> ) } diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index f8b4f63924..9cc5af589b 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -12,6 +12,7 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import cn from '@/utils/classnames' +import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { source: PluginSource @@ -40,6 +41,8 @@ const OperationDropdown: FC<Props> = ({ setOpen(!openRef.current) }, [setOpen]) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + return ( <PortalToFollowElem open={open} @@ -77,13 +80,13 @@ const OperationDropdown: FC<Props> = ({ className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover' >{t('plugin.detailPanel.operation.checkUpdate')}</div> )} - {(source === PluginSource.marketplace || source === PluginSource.github) && ( + {(source === PluginSource.marketplace || source === PluginSource.github) && enable_marketplace && ( <a href={detailUrl} target='_blank' className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'> <span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span> <RiArrowRightUpLine className='h-3.5 w-3.5 shrink-0 text-text-tertiary' /> </a> )} - {(source === PluginSource.marketplace || source === PluginSource.github) && ( + {(source === PluginSource.marketplace || source === PluginSource.github) && enable_marketplace && ( <div className='my-1 h-px bg-divider-subtle'></div> )} <div diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index ca802414f3..15401f1057 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import Link from 'next/link' import { RiArrowLeftLine, - RiArrowRightUpLine, } from '@remixicon/react' import { PortalToFollowElem, @@ -15,6 +14,7 @@ import { import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger' import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' +import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' import Button from '@/app/components/base/button' import Indicator from '@/app/components/header/indicator' import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form' @@ -23,13 +23,13 @@ import Textarea from '@/app/components/base/textarea' import Divider from '@/app/components/base/divider' import TabSlider from '@/app/components/base/tab-slider-plain' import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' -import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { useAppContext } from '@/context/app-context' import { useAllBuiltInTools, useAllCustomTools, + useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, useUpdateProviderCredentials, @@ -54,15 +54,9 @@ type Props = { scope?: string value?: ToolValue selectedTools?: ToolValue[] + onSelect: (tool: ToolValue) => void + onSelectMultiple?: (tool: ToolValue[]) => void isEdit?: boolean - onSelect: (tool: { - provider_name: string - tool_name: string - tool_label: string - settings?: Record<string, any> - parameters?: Record<string, any> - extra?: Record<string, any> - }) => void onDelete?: () => void supportEnableSwitch?: boolean supportAddCustomTool?: boolean @@ -74,6 +68,7 @@ type Props = { nodeOutputVars: NodeOutPutVar[], availableNodes: Node[], nodeId?: string, + canChooseMCPTool?: boolean, } const ToolSelector: FC<Props> = ({ value, @@ -83,6 +78,7 @@ const ToolSelector: FC<Props> = ({ placement = 'left', offset = 4, onSelect, + onSelectMultiple, onDelete, scope, supportEnableSwitch, @@ -94,6 +90,7 @@ const ToolSelector: FC<Props> = ({ nodeOutputVars, availableNodes, nodeId = '', + canChooseMCPTool, }) => { const { t } = useTranslation() const [isShow, onShowChange] = useState(false) @@ -105,6 +102,7 @@ const ToolSelector: FC<Props> = ({ const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() @@ -112,18 +110,19 @@ const ToolSelector: FC<Props> = ({ const { inMarketPlace, manifest } = usePluginInstalledCheck(value?.provider_name) const currentProvider = useMemo(() => { - const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] + const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])] return mergedTools.find((toolWithProvider) => { return toolWithProvider.id === value?.provider_name }) - }, [value, buildInTools, customTools, workflowTools]) + }, [value, buildInTools, customTools, workflowTools, mcpTools]) const [isShowChooseTool, setIsShowChooseTool] = useState(false) - const handleSelectTool = (tool: ToolDefaultValue) => { + const getToolValue = (tool: ToolDefaultValue) => { const settingValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form !== 'llm') as any)) const paramValues = generateFormValue(tool.params, toolParametersToFormSchemas(tool.paramSchemas.filter(param => param.form === 'llm') as any), true) - const toolValue = { + return { provider_name: tool.provider_id, + provider_show_name: tool.provider_name, type: tool.provider_type, tool_name: tool.tool_name, tool_label: tool.tool_label, @@ -136,9 +135,16 @@ const ToolSelector: FC<Props> = ({ }, schemas: tool.paramSchemas, } + } + const handleSelectTool = (tool: ToolDefaultValue) => { + const toolValue = getToolValue(tool) onSelect(toolValue) // setIsShowChooseTool(false) } + const handleSelectMultipleTool = (tool: ToolDefaultValue[]) => { + const toolValues = tool.map(item => getToolValue(item)) + onSelectMultiple?.(toolValues) + } const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { onSelect({ @@ -169,7 +175,6 @@ const ToolSelector: FC<Props> = ({ const handleSettingsFormChange = (v: Record<string, any>) => { const newValue = getStructureValue(v) - const toolValue = { ...value, settings: newValue, @@ -250,7 +255,9 @@ const ToolSelector: FC<Props> = ({ <ToolItem open={isShow} icon={currentProvider?.icon || manifestIcon} + isMCPTool={currentProvider?.type === CollectionType.mcp} providerName={value.provider_name} + providerShowName={value.provider_show_name} toolLabel={value.tool_label || value.tool_name} showSwitch={supportEnableSwitch} switchValue={value.enabled} @@ -272,10 +279,11 @@ const ToolSelector: FC<Props> = ({ </p> </div> } + canChooseMCPTool={canChooseMCPTool} /> )} </PortalToFollowElemTrigger> - <PortalToFollowElemContent className='z-[1000]'> + <PortalToFollowElemContent> <div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', !isShowSettingAuth && 'overflow-y-auto pb-2')}> {!isShowSettingAuth && ( <> @@ -285,7 +293,6 @@ const ToolSelector: FC<Props> = ({ <div className='flex flex-col gap-1'> <div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div> <ToolPicker - panelClassName='w-[328px]' placement='bottom' offset={offset} trigger={ @@ -300,8 +307,10 @@ const ToolSelector: FC<Props> = ({ disabled={false} supportAddCustomTool onSelect={handleSelectTool} + onSelectMultiple={handleSelectMultipleTool} scope={scope} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> </div> <div className='flex flex-col gap-1'> @@ -390,24 +399,13 @@ const ToolSelector: FC<Props> = ({ {/* user settings form */} {(currType === 'settings' || userSettingsOnly) && ( <div className='px-4 py-2'> - <Form + <ToolForm + inPanel + readOnly={false} + nodeId={nodeId} + schema={settingsFormSchemas as any} value={getPlainValue(value?.settings || {})} onChange={handleSettingsFormChange} - formSchemas={settingsFormSchemas as any} - isEditMode={true} - showOnVariableMap={{}} - validating={false} - inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover' - fieldMoreInfo={item => item.url - ? (<a - href={item.url} - target='_blank' rel='noopener noreferrer' - className='inline-flex items-center text-xs text-text-accent' - > - {t('tools.howToGet')} - <RiArrowRightUpLine className='ml-1 h-3 w-3' /> - </a>) - : null} /> </div> )} diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx index 750a8cfff6..98ad490348 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx @@ -3,25 +3,34 @@ import { useTranslation } from 'react-i18next' import produce from 'immer' import { RiArrowRightUpLine, + RiBracesLine, } from '@remixicon/react' import Tooltip from '@/app/components/base/tooltip' import Switch from '@/app/components/base/switch' -import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' +import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input' +import Input from '@/app/components/base/input' +import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/components/form-input-type-switch' +import FormInputBoolean from '@/app/components/workflow/nodes/_base/components/form-input-boolean' +import { SimpleSelect } from '@/app/components/base/select' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Node } from 'reactflow' import type { NodeOutPutVar, ValueSelector, - Var, } from '@/app/components/workflow/types' import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { VarType } from '@/app/components/workflow/types' import cn from '@/utils/classnames' +import { useBoolean } from 'ahooks' +import SchemaModal from './schema-modal' +import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' type Props = { value: Record<string, any> @@ -42,73 +51,46 @@ const ReasoningConfigForm: React.FC<Props> = ({ }) => { const { t } = useTranslation() const language = useLanguage() - const handleAutomatic = (key: string, val: any) => { + const getVarKindType = (type: FormTypeEnum) => { + if (type === FormTypeEnum.file || type === FormTypeEnum.files) + return VarKindType.variable + if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber || type === FormTypeEnum.array || type === FormTypeEnum.object) + return VarKindType.constant + if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) + return VarKindType.mixed + } + + const handleAutomatic = (key: string, val: any, type: FormTypeEnum) => { onChange({ ...value, [key]: { - value: val ? null : value[key]?.value, + value: val ? null : { type: getVarKindType(type), value: null }, auto: val ? 1 : 0, }, }) } - - const [inputsIsFocus, setInputsIsFocus] = useState<Record<string, boolean>>({}) - const handleInputFocus = useCallback((variable: string) => { - return (value: boolean) => { - setInputsIsFocus((prev) => { - return { - ...prev, - [variable]: value, - } - }) - } - }, []) - const handleNotMixedTypeChange = useCallback((variable: string) => { - return (varValue: ValueSelector | string, varKindType: VarKindType) => { - const newValue = produce(value, (draft: ToolVarInputs) => { - const target = draft[variable].value - if (target) { - target.type = varKindType - target.value = varValue - } - else { - draft[variable].value = { - type: varKindType, - value: varValue, - } - } - }) - onChange(newValue) - } - }, [value, onChange]) - const handleMixedTypeChange = useCallback((variable: string) => { - return (itemValue: string) => { - const newValue = produce(value, (draft: ToolVarInputs) => { - const target = draft[variable].value - if (target) { - target.value = itemValue - } - else { - draft[variable].value = { - type: VarKindType.mixed, - value: itemValue, - } + const handleTypeChange = useCallback((variable: string, defaultValue: any) => { + return (newType: VarKindType) => { + const res = produce(value, (draft: ToolVarInputs) => { + draft[variable].value = { + type: newType, + value: newType === VarKindType.variable ? '' : defaultValue, } }) - onChange(newValue) + onChange(res) } - }, [value, onChange]) - const handleFileChange = useCallback((variable: string) => { - return (varValue: ValueSelector | string) => { - const newValue = produce(value, (draft: ToolVarInputs) => { + }, [onChange, value]) + const handleValueChange = useCallback((variable: string, varType: FormTypeEnum) => { + return (newValue: any) => { + const res = produce(value, (draft: ToolVarInputs) => { draft[variable].value = { - type: VarKindType.variable, - value: varValue, + type: getVarKindType(varType), + value: newValue, } }) - onChange(newValue) + onChange(res) } - }, [value, onChange]) + }, [onChange, value]) const handleAppChange = useCallback((variable: string) => { return (app: { app_id: string @@ -132,9 +114,29 @@ const ReasoningConfigForm: React.FC<Props> = ({ onChange(newValue) } }, [onChange, value]) + const handleVariableSelectorChange = useCallback((variable: string) => { + return (newValue: ValueSelector | string) => { + const res = produce(value, (draft: ToolVarInputs) => { + draft[variable].value = { + type: VarKindType.variable, + value: newValue, + } + }) + onChange(res) + } + }, [onChange, value]) - const renderField = (schema: any) => { + const [isShowSchema, { + setTrue: showSchema, + setFalse: hideSchema, + }] = useBoolean(false) + + const [schema, setSchema] = useState<SchemaRoot | null>(null) + const [schemaRootName, setSchemaRootName] = useState<string>('') + + const renderField = (schema: any, showSchema: (schema: SchemaRoot, rootName: string) => void) => { const { + default: defaultValue, variable, label, required, @@ -142,6 +144,9 @@ const ReasoningConfigForm: React.FC<Props> = ({ type, scope, url, + input_schema, + placeholder, + options, } = schema const auto = value[variable]?.auto const tooltipContent = (tooltip && ( @@ -149,89 +154,150 @@ const ReasoningConfigForm: React.FC<Props> = ({ popupContent={<div className='w-[200px]'> {tooltip[language] || tooltip.en_US} </div>} - triggerClassName='ml-1 w-4 h-4' + triggerClassName='ml-0.5 w-4 h-4' asChild={false} /> )) const varInput = value[variable].value + const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput const isNumber = type === FormTypeEnum.textNumber - const isSelect = type === FormTypeEnum.select + const isObject = type === FormTypeEnum.object + const isArray = type === FormTypeEnum.array + const isShowJSONEditor = isObject || isArray const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files + const isBoolean = type === FormTypeEnum.boolean + const isSelect = type === FormTypeEnum.select const isAppSelector = type === FormTypeEnum.appSelector const isModelSelector = type === FormTypeEnum.modelSelector - // const isToolSelector = type === FormTypeEnum.toolSelector - const isString = !isNumber && !isSelect && !isFile && !isAppSelector && !isModelSelector + const showTypeSwitch = isNumber || isObject || isArray + const isConstant = varInput?.type === VarKindType.constant || !varInput?.type + const showVariableSelector = isFile || varInput?.type === VarKindType.variable + const targetVarType = () => { + if (isString) + return VarType.string + else if (isNumber) + return VarType.number + else if (type === FormTypeEnum.files) + return VarType.arrayFile + else if (type === FormTypeEnum.file) + return VarType.file + else if (isBoolean) + return VarType.boolean + else if (isObject) + return VarType.object + else if (isArray) + return VarType.arrayObject + else + return VarType.string + } + const getFilterVar = () => { + if (isNumber) + return (varPayload: any) => varPayload.type === VarType.number + else if (isString) + return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + else if (isFile) + return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type) + else if (isBoolean) + return (varPayload: any) => varPayload.type === VarType.boolean + else if (isObject) + return (varPayload: any) => varPayload.type === VarType.object + else if (isArray) + return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type) + return undefined + } + return ( - <div key={variable} className='space-y-1'> + <div key={variable} className='space-y-0.5'> <div className='system-sm-semibold flex items-center justify-between py-2 text-text-secondary'> - <div className='flex items-center space-x-2'> - <span className={cn('code-sm-semibold text-text-secondary')}>{label[language] || label.en_US}</span> + <div className='flex items-center'> + <span className={cn('code-sm-semibold max-w-[140px] truncate text-text-secondary')} title={label[language] || label.en_US}>{label[language] || label.en_US}</span> {required && ( <span className='ml-1 text-red-500'>*</span> )} {tooltipContent} + <span className='system-xs-regular mx-1 text-text-quaternary'>·</span> + <span className='system-xs-regular text-text-tertiary'>{targetVarType()}</span> + {isShowJSONEditor && ( + <Tooltip + popupContent={<div className='system-xs-medium text-text-secondary'> + {t('workflow.nodes.agent.clickToViewParameterSchema')} + </div>} + asChild={false}> + <div + className='ml-0.5 cursor-pointer rounded-[4px] p-px text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary' + onClick={() => showSchema(input_schema as SchemaRoot, label[language] || label.en_US)} + > + <RiBracesLine className='size-3.5'/> + </div> + </Tooltip> + )} + </div> - <div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto)}> + <div className='flex cursor-pointer items-center gap-1 rounded-[6px] border border-divider-subtle bg-background-default-lighter px-2 py-1 hover:bg-state-base-hover' onClick={() => handleAutomatic(variable, !auto, type)}> <span className='system-xs-medium text-text-secondary'>{t('plugin.detailPanel.toolSelector.auto')}</span> <Switch size='xs' defaultValue={!!auto} - onChange={val => handleAutomatic(variable, val)} + onChange={val => handleAutomatic(variable, val, type)} /> </div> </div> {auto === 0 && ( - <> + <div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}> + {showTypeSwitch && ( + <FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange(variable, defaultValue)}/> + )} {isString && ( - <Input - className={cn(inputsIsFocus[variable] ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'rounded-lg border px-3 py-[6px]')} + <MixedVariableTextInput value={varInput?.value as string || ''} - onChange={handleMixedTypeChange(variable)} + onChange={handleValueChange(variable, type)} nodesOutputVars={nodeOutputVars} availableNodes={availableNodes} - onFocusChange={handleInputFocus(variable)} - placeholder={t('workflow.nodes.http.insertVarPlaceholder')!} - placeholderClassName='!leading-[21px]' /> )} - {/* {isString && ( - <VarReferencePicker - zIndex={1001} - readonly={false} - isShowNodeName - nodeId={nodeId} + {isNumber && isConstant && ( + <Input + className='h-8 grow' + type='number' value={varInput?.value || ''} - onChange={handleNotMixedTypeChange(variable)} - defaultVarKindType={VarKindType.variable} - filterVar={(varPayload: Var) => varPayload.type === VarType.number || varPayload.type === VarType.secret || varPayload.type === VarType.string} + onChange={handleValueChange(variable, type)} + placeholder={placeholder?.[language] || placeholder?.en_US} /> - )} */} - {(isNumber || isSelect) && ( - <VarReferencePicker - zIndex={1001} - readonly={false} - isShowNodeName - nodeId={nodeId} - value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} - onChange={handleNotMixedTypeChange(variable)} - defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} - isSupportConstantValue - filterVar={isNumber ? (varPayload: Var) => varPayload.type === schema._type : undefined} - availableVars={isSelect ? nodeOutputVars : undefined} - schema={schema} + )} + {isBoolean && ( + <FormInputBoolean + value={varInput?.value as boolean} + onChange={handleValueChange(variable, type)} /> )} - {isFile && ( - <VarReferencePicker - zIndex={1001} - readonly={false} - isShowNodeName - nodeId={nodeId} - value={varInput?.value || []} - onChange={handleFileChange(variable)} - defaultVarKindType={VarKindType.variable} - filterVar={(varPayload: Var) => varPayload.type === VarType.file || varPayload.type === VarType.arrayFile} + {isSelect && ( + <SimpleSelect + wrapperClassName='h-8 grow' + defaultValue={varInput?.value} + items={options.filter((option: { show_on: any[] }) => { + if (option.show_on.length) + return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) + + return true + }).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))} + onSelect={item => handleValueChange(variable, type)(item.value as string)} + placeholder={placeholder?.[language] || placeholder?.en_US} /> )} + {isShowJSONEditor && isConstant && ( + <div className='mt-1 w-full'> + <CodeEditor + title='JSON' + value={varInput?.value as any} + isExpand + isInNode + height={100} + language={CodeLanguage.json} + onChange={handleValueChange(variable, type)} + className='w-full' + placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>} + /> + </div> + )} {isAppSelector && ( <AppSelector disabled={false} @@ -250,7 +316,21 @@ const ReasoningConfigForm: React.FC<Props> = ({ scope={scope} /> )} - </> + {showVariableSelector && ( + <VarReferencePicker + zIndex={1001} + className='h-8 grow' + readonly={false} + isShowNodeName + nodeId={nodeId} + value={varInput?.value || []} + onChange={handleVariableSelectorChange(variable)} + filterVar={getFilterVar()} + schema={schema} + valueTypePlaceHolder={targetVarType()} + /> + )} + </div> )} {url && ( <a @@ -267,7 +347,19 @@ const ReasoningConfigForm: React.FC<Props> = ({ } return ( <div className='space-y-3 px-4 py-2'> - {schemas.map(schema => renderField(schema))} + {!isShowSchema && schemas.map(schema => renderField(schema, (s: SchemaRoot, rootName: string) => { + setSchema(s) + setSchemaRootName(rootName) + showSchema() + }))} + {isShowSchema && ( + <SchemaModal + isShow={isShowSchema} + schema={schema!} + rootName={schemaRootName} + onClose={hideSchema} + /> + )} </div> ) } diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal.tsx new file mode 100644 index 0000000000..cd4cf71bac --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal.tsx @@ -0,0 +1,59 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import Modal from '@/app/components/base/modal' +import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor' +import type { SchemaRoot } from '@/app/components/workflow/nodes/llm/types' +import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context' +import { useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' + +type Props = { + isShow: boolean + schema: SchemaRoot + rootName: string + onClose: () => void +} + +const SchemaModal: FC<Props> = ({ + isShow, + schema, + rootName, + onClose, +}) => { + const { t } = useTranslation() + return ( + <Modal + isShow={isShow} + onClose={onClose} + className='max-w-[960px] p-0' + wrapperClassName='z-[9999]' + > + <div className='pb-6'> + {/* Header */} + <div className='relative flex p-6 pb-3 pr-14'> + <div className='title-2xl-semi-bold grow truncate text-text-primary'> + {t('workflow.nodes.agent.parameterSchema')} + </div> + <div className='absolute right-5 top-5 flex h-8 w-8 items-center justify-center p-1.5' onClick={onClose}> + <RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' /> + </div> + </div> + {/* Content */} + <div className='flex max-h-[700px] overflow-y-auto px-6 py-2'> + <MittProvider> + <VisualEditorContextProvider> + <VisualEditor + className='w-full' + schema={schema} + rootName={rootName} + readOnly + ></VisualEditor> + </VisualEditorContextProvider> + </MittProvider> + </div> + </div> + </Modal> + ) +} +export default React.memo(SchemaModal) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx index d74fccf968..6fa0ce57ab 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/tool-item.tsx @@ -17,10 +17,13 @@ import { ToolTipContent } from '@/app/components/base/tooltip/content' import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version' import cn from '@/utils/classnames' +import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip' type Props = { icon?: any providerName?: string + isMCPTool?: boolean + providerShowName?: string toolLabel?: string showSwitch?: boolean switchValue?: boolean @@ -35,11 +38,14 @@ type Props = { onInstall?: () => void versionMismatch?: boolean open: boolean + canChooseMCPTool?: boolean, } const ToolItem = ({ open, icon, + isMCPTool, + providerShowName, providerName, toolLabel, showSwitch, @@ -54,11 +60,13 @@ const ToolItem = ({ isError, errorTip, versionMismatch, + canChooseMCPTool, }: Props) => { const { t } = useTranslation() - const providerNameText = providerName?.split('/').pop() + const providerNameText = isMCPTool ? providerShowName : providerName?.split('/').pop() const isTransparent = uninstalled || versionMismatch || isError const [isDeleting, setIsDeleting] = useState(false) + const isShowCanNotChooseMCPTip = isMCPTool && !canChooseMCPTool return ( <div className={cn( @@ -67,7 +75,7 @@ const ToolItem = ({ isDeleting && 'border-state-destructive-border shadow-xs hover:bg-state-destructive-hover', )}> {icon && ( - <div className={cn('shrink-0', isTransparent && 'opacity-50')}> + <div className={cn('shrink-0', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}> {typeof icon === 'string' && <div className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge bg-cover bg-center' style={{ backgroundImage: `url(${icon})` }} />} {typeof icon !== 'string' && <AppIcon className='h-7 w-7 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge' size='xs' icon={icon?.content} background={icon?.background} />} </div> @@ -75,18 +83,19 @@ const ToolItem = ({ {!icon && ( <div className={cn( 'flex h-7 w-7 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle', + isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30', )}> <div className='flex h-5 w-5 items-center justify-center opacity-35'> <Group className='text-text-tertiary' /> </div> </div> )} - <div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50')}> + <div className={cn('grow truncate pl-0.5', isTransparent && 'opacity-50', isShowCanNotChooseMCPTip && 'opacity-30')}> <div className='system-2xs-medium-uppercase text-text-tertiary'>{providerNameText}</div> <div className='system-xs-medium text-text-secondary'>{toolLabel}</div> </div> <div className='hidden items-center gap-1 group-hover:flex'> - {!noAuth && !isError && !uninstalled && !versionMismatch && ( + {!noAuth && !isError && !uninstalled && !versionMismatch && !isShowCanNotChooseMCPTip && ( <ActionButton> <RiEqualizer2Line className='h-4 w-4' /> </ActionButton> @@ -103,7 +112,7 @@ const ToolItem = ({ <RiDeleteBinLine className='h-4 w-4' /> </div> </div> - {!isError && !uninstalled && !noAuth && !versionMismatch && showSwitch && ( + {!isError && !uninstalled && !noAuth && !versionMismatch && !isShowCanNotChooseMCPTip && showSwitch && ( <div className='mr-1' onClick={e => e.stopPropagation()}> <Switch size='md' @@ -112,6 +121,9 @@ const ToolItem = ({ /> </div> )} + {isShowCanNotChooseMCPTip && ( + <McpToolNotSupportTooltip /> + )} {!isError && !uninstalled && !versionMismatch && noAuth && ( <Button variant='secondary' size='small' onClick={onAuth}> {t('tools.notAuthorized')} @@ -149,7 +161,6 @@ const ToolItem = ({ {isError && ( <Tooltip popupContent={errorTip} - needsDelay > <div> <RiErrorWarningFill className='h-4 w-4 text-text-destructive' /> diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index b2d5f91caa..058f1783f2 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -21,13 +21,15 @@ import OrgInfo from '../card/base/org-info' import Title from '../card/base/title' import Action from './action' import cn from '@/utils/classnames' -import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' +import { API_PREFIX } from '@/config' import { useSingleCategories } from '../hooks' import { useRenderI18nObject } from '@/hooks/use-i18n' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { useAppContext } from '@/context/app-context' import { gte } from 'semver' import Tooltip from '@/app/components/base/tooltip' +import { getMarketplaceUrl } from '@/utils/var' +import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { className?: string @@ -74,6 +76,7 @@ const PluginItem: FC<Props> = ({ const getValueFromI18nObject = useRenderI18nObject() const title = getValueFromI18nObject(label) const descriptionText = getValueFromI18nObject(description) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) return ( <div @@ -164,9 +167,9 @@ const PluginItem: FC<Props> = ({ </a> </> } - {source === PluginSource.marketplace + {source === PluginSource.marketplace && enable_marketplace && <> - <a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='flex items-center gap-0.5'> + <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='flex items-center gap-0.5'> <div className='system-2xs-medium-uppercase text-text-tertiary'>{t('plugin.from')} <span className='text-text-secondary'>marketplace</span></div> <RiArrowRightUpLine className='h-3 w-3 text-text-tertiary' /> </a> diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx index d17c4f420b..a84427eca4 100644 --- a/web/app/components/plugins/plugin-page/empty/index.tsx +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -1,4 +1,5 @@ -import React, { useMemo, useRef, useState } from 'react' +'use client' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' import { Github } from '@/app/components/base/icons/src/vender/solid/general' @@ -14,12 +15,18 @@ import { noop } from 'lodash-es' import { useGlobalPublicStore } from '@/context/global-public-context' import Button from '@/app/components/base/button' +type InstallMethod = { + icon: React.FC<{ className?: string }> + text: string + action: string +} + const Empty = () => { const { t } = useTranslation() const fileInputRef = useRef<HTMLInputElement>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null) - const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures) const setActiveTab = usePluginPageContext(v => v.setActiveTab) const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { @@ -39,6 +46,22 @@ const Empty = () => { return t('plugin.list.notFound') }, [pluginList?.plugins.length, t, filters.categories.length, filters.tags.length, filters.searchQuery]) + const [installMethods, setInstallMethods] = useState<InstallMethod[]>([]) + useEffect(() => { + const methods = [] + if (enable_marketplace) + methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }) + + if (plugin_installation_permission.restrict_to_marketplace_only) { + setInstallMethods(methods) + } + else { + methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' }) + methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' }) + setInstallMethods(methods) + } + }, [plugin_installation_permission, enable_marketplace, t]) + return ( <div className='relative z-0 w-full grow'> {/* skeleton */} @@ -71,15 +94,7 @@ const Empty = () => { accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} /> <div className='flex w-full flex-col gap-y-1'> - {[ - ...( - (enable_marketplace) - ? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }] - : [] - ), - { icon: Github, text: t('plugin.list.source.github'), action: 'github' }, - { icon: FileZip, text: t('plugin.list.source.local'), action: 'local' }, - ].map(({ icon: Icon, text, action }) => ( + {installMethods.map(({ icon: Icon, text, action }) => ( <Button key={action} className='justify-start gap-x-0.5 px-3' diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index e769c8e29a..94fd3fee9b 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -35,7 +35,7 @@ import type { PluginDeclaration, PluginManifestInMarket } from '../types' import { sleep } from '@/utils' import { getDocsUrl } from '@/app/components/plugins/utils' import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins' -import { marketplaceApiPrefix } from '@/config' +import { MARKETPLACE_API_PREFIX } from '@/config' import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import I18n from '@/context/i18n' import { noop } from 'lodash-es' @@ -106,7 +106,7 @@ const PluginPage = ({ setManifest({ ...plugin, version: version.version, - icon: `${marketplaceApiPrefix}/plugins/${plugin.org}/${plugin.name}/icon`, + icon: `${MARKETPLACE_API_PREFIX}/plugins/${plugin.org}/${plugin.name}/icon`, }) showInstallFromMarketplace() return @@ -206,7 +206,7 @@ const PluginPage = ({ variant='secondary-accent' > <RiBookOpenLine className='mr-1 h-4 w-4' /> - {t('plugin.submitPlugin')} + {t('plugin.publishPlugins')} </Button> </Link> <div className='mx-1 h-3.5 w-[1px] shrink-0 bg-divider-regular'></div> diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index abe4f9cb6a..be62adb310 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' import Button from '@/app/components/base/button' import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' @@ -22,6 +22,13 @@ import { useGlobalPublicStore } from '@/context/global-public-context' type Props = { onSwitchToMarketplaceTab: () => void } + +type InstallMethod = { + icon: React.FC<{ className?: string }> + text: string + action: string +} + const InstallPluginDropdown = ({ onSwitchToMarketplaceTab, }: Props) => { @@ -30,7 +37,7 @@ const InstallPluginDropdown = ({ const [isMenuOpen, setIsMenuOpen] = useState(false) const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null) - const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures) const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0] @@ -54,6 +61,22 @@ const InstallPluginDropdown = ({ // console.log(res) // } + const [installMethods, setInstallMethods] = useState<InstallMethod[]>([]) + useEffect(() => { + const methods = [] + if (enable_marketplace) + methods.push({ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }) + + if (plugin_installation_permission.restrict_to_marketplace_only) { + setInstallMethods(methods) + } + else { + methods.push({ icon: Github, text: t('plugin.source.github'), action: 'github' }) + methods.push({ icon: FileZip, text: t('plugin.source.local'), action: 'local' }) + setInstallMethods(methods) + } + }, [plugin_installation_permission, enable_marketplace, t]) + return ( <PortalToFollowElem open={isMenuOpen} @@ -84,15 +107,7 @@ const InstallPluginDropdown = ({ accept={SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS} /> <div className='w-full'> - {[ - ...( - (enable_marketplace) - ? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }] - : [] - ), - { icon: Github, text: t('plugin.source.github'), action: 'github' }, - { icon: FileZip, text: t('plugin.source.local'), action: 'local' }, - ].map(({ icon: Icon, text, action }) => ( + {installMethods.map(({ icon: Icon, text, action }) => ( <div key={action} className='flex w-full !cursor-pointer items-center gap-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover' diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 231763aaab..cd4d5e9ece 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -94,7 +94,11 @@ export type PluginManifestInMarket = { introduction: string verified: boolean install_count: number - badges: string[] + badges: string[], + verification: { + authorized_category: 'langgenius' | 'partner' | 'community' + }, + from: Dependency['type'] } export type PluginDetail = { @@ -145,7 +149,11 @@ export type Plugin = { settings: CredentialFormSchemaBase[] } tags: { name: string }[] - badges: string[] + badges: string[], + verification: { + authorized_category: 'langgenius' | 'partner' | 'community' + }, + from: Dependency['type'] } export enum PermissionType { @@ -454,9 +462,14 @@ export type StrategyDeclaration = { strategies: StrategyDetail[] } +export type PluginMeta = { + version: string // the version of dify sdk +} + export type StrategyPluginDetail = { provider: string plugin_unique_identifier: string plugin_id: string declaration: StrategyDeclaration + meta: PluginMeta } diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 6fd6d17278..4be6b18958 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -9,7 +9,7 @@ import { import { useBoolean } from 'ahooks' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' -import { checkOrSetAccessToken } from '../utils' +import { checkOrSetAccessToken, removeAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' @@ -85,14 +85,6 @@ const TextGeneration: FC<IMainProps> = ({ const router = useRouter() const pathname = usePathname() - useEffect(() => { - const params = new URLSearchParams(searchParams) - if (params.has('mode')) { - params.delete('mode') - router.replace(`${pathname}?${params.toString()}`) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) // Notice this situation isCallBatchAPI but not in batch tab const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) @@ -536,14 +528,31 @@ const TextGeneration: FC<IMainProps> = ({ </div> ) + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { return ( <div className='flex h-screen items-center'> <Loading type='app' /> </div>) } - if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) - return <AppUnavailable code={403} unknownReason='no permission.' /> + if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { + return <div className='flex h-full flex-col items-center justify-center gap-y-2'> + <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> + {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} + </div> + } return ( <div className={cn( diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts index d793d48b48..8a897ab59a 100644 --- a/web/app/components/share/utils.ts +++ b/web/app/components/share/utils.ts @@ -57,22 +57,6 @@ export const setAccessToken = (sharedToken: string, token: string, user_id?: str } export const removeAccessToken = () => { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - - const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) - let accessTokenJson = getInitialTokenV2() - try { - accessTokenJson = JSON.parse(accessToken) - if (isTokenV1(accessTokenJson)) - accessTokenJson = getInitialTokenV2() - } - catch { - - } - - localStorage.removeItem(CONVERSATION_ID_INFO) + localStorage.removeItem('token') localStorage.removeItem('webapp_access_token') - - delete accessTokenJson[sharedToken] - localStorage.setItem('token', JSON.stringify(accessTokenJson)) } diff --git a/web/app/components/tools/add-tool-modal/empty.tsx b/web/app/components/tools/add-tool-modal/empty.tsx index 540d263e99..7390249dc1 100644 --- a/web/app/components/tools/add-tool-modal/empty.tsx +++ b/web/app/components/tools/add-tool-modal/empty.tsx @@ -1,19 +1,48 @@ 'use client' -import { useSearchParams } from 'next/navigation' import { useTranslation } from 'react-i18next' -const Empty = () => { +import { ToolTypeEnum } from '../../workflow/block-selector/types' +import { RiArrowRightUpLine } from '@remixicon/react' +import Link from 'next/link' +import cn from '@/utils/classnames' +import { NoToolPlaceholder } from '../../base/icons/src/vender/other' +type Props = { + type?: ToolTypeEnum + isAgent?: boolean +} + +const getLink = (type?: ToolTypeEnum) => { + switch (type) { + case ToolTypeEnum.Custom: + return '/tools?category=api' + case ToolTypeEnum.MCP: + return '/tools?category=mcp' + default: + return '/tools?category=api' + } +} +const Empty = ({ + type, + isAgent, +}: Props) => { const { t } = useTranslation() - const searchParams = useSearchParams() + + const hasLink = type && [ToolTypeEnum.Custom, ToolTypeEnum.MCP].includes(type) + const Comp = (hasLink ? Link : 'div') as any + const linkProps = hasLink ? { href: getLink(type), target: '_blank' } : {} + const renderType = isAgent ? 'agent' : type + const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title` return ( - <div className='flex flex-col items-center'> - <div className="h-[149px] w-[163px] shrink-0 bg-[url('~@/app/components/tools/add-tool-modal/empty.png')] bg-cover bg-no-repeat"></div> - <div className='mb-1 text-[13px] font-medium leading-[18px] text-text-primary'> - {t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTitle' : 'emptyTitleCustom'}`)} - </div> - <div className='text-[13px] leading-[18px] text-text-tertiary'> - {t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTip' : 'emptyTipCustom'}`)} + <div className='flex h-[336px] flex-col items-center justify-center'> + <NoToolPlaceholder /> + <div className='mb-1 mt-2 text-[13px] font-medium leading-[18px] text-text-primary'> + {hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'} </div> + {(!isAgent && hasTitle) && ( + <Comp className={cn('flex items-center text-[13px] leading-[18px] text-text-tertiary', hasLink && 'cursor-pointer hover:text-text-accent')} {...linkProps}> + {t(`tools.addToolModal.${renderType}.tip`)} {hasLink && <RiArrowRightUpLine className='ml-0.5 h-3 w-3' />} + </Comp> + )} </div> ) } diff --git a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx index cbf1048b09..f0ad13f9b1 100644 --- a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx @@ -68,23 +68,34 @@ const ConfigCredential: FC<Props> = ({ text={t('tools.createTool.authMethod.types.none')} value={AuthType.none} isChecked={tempCredential.auth_type === AuthType.none} - onClick={value => setTempCredential({ ...tempCredential, auth_type: value as AuthType })} + onClick={value => setTempCredential({ + auth_type: value as AuthType, + })} /> <SelectItem - text={t('tools.createTool.authMethod.types.api_key')} - value={AuthType.apiKey} - isChecked={tempCredential.auth_type === AuthType.apiKey} + text={t('tools.createTool.authMethod.types.api_key_header')} + value={AuthType.apiKeyHeader} + isChecked={tempCredential.auth_type === AuthType.apiKeyHeader} onClick={value => setTempCredential({ - ...tempCredential, auth_type: value as AuthType, api_key_header: tempCredential.api_key_header || 'Authorization', api_key_value: tempCredential.api_key_value || '', api_key_header_prefix: tempCredential.api_key_header_prefix || AuthHeaderPrefix.custom, })} /> + <SelectItem + text={t('tools.createTool.authMethod.types.api_key_query')} + value={AuthType.apiKeyQuery} + isChecked={tempCredential.auth_type === AuthType.apiKeyQuery} + onClick={value => setTempCredential({ + auth_type: value as AuthType, + api_key_query_param: tempCredential.api_key_query_param || 'key', + api_key_value: tempCredential.api_key_value || '', + })} + /> </div> </div> - {tempCredential.auth_type === AuthType.apiKey && ( + {tempCredential.auth_type === AuthType.apiKeyHeader && ( <> <div> <div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.authHeaderPrefix.title')}</div> @@ -136,6 +147,35 @@ const ConfigCredential: FC<Props> = ({ /> </div> </>)} + {tempCredential.auth_type === AuthType.apiKeyQuery && ( + <> + <div> + <div className='system-sm-medium flex items-center py-2 text-text-primary'> + {t('tools.createTool.authMethod.queryParam')} + <Tooltip + popupContent={ + <div className='w-[261px] text-text-tertiary'> + {t('tools.createTool.authMethod.queryParamTooltip')} + </div> + } + triggerClassName='ml-0.5 w-4 h-4' + /> + </div> + <Input + value={tempCredential.api_key_query_param} + onChange={e => setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.queryParamPlaceholder')!} + /> + </div> + <div> + <div className='system-sm-medium py-2 text-text-primary'>{t('tools.createTool.authMethod.value')}</div> + <Input + value={tempCredential.api_key_value} + onChange={e => setTempCredential({ ...tempCredential, api_key_value: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!} + /> + </div> + </>)} </div> diff --git a/web/app/components/tools/edit-custom-collection-modal/modal.tsx b/web/app/components/tools/edit-custom-collection-modal/modal.tsx index 190c72790e..ce7ba8a735 100644 --- a/web/app/components/tools/edit-custom-collection-modal/modal.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/modal.tsx @@ -184,6 +184,7 @@ const EditCustomCollectionModal: FC<Props> = ({ onClose={onHide} closable className='!h-[calc(100vh-16px)] !max-w-[630px] !p-0' + wrapperClassName='z-[1000]' > <div className='flex h-full flex-col'> <div className='ml-6 mt-6 text-base font-semibold text-text-primary'> diff --git a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx index 0079bad7c7..a03364aa3d 100644 --- a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx @@ -31,10 +31,13 @@ const TestApi: FC<Props> = ({ const language = getLanguage(locale) const [credentialsModalShow, setCredentialsModalShow] = useState(false) const [tempCredential, setTempCredential] = React.useState<Credential>(customCollection.credentials) + const [testing, setTesting] = useState(false) const [result, setResult] = useState<string>('') const { operation_id: toolName, parameters } = tool const [parametersValue, setParametersValue] = useState<Record<string, string>>({}) const handleTest = async () => { + if (testing) return + setTesting(true) // clone test schema const credentials = JSON.parse(JSON.stringify(tempCredential)) as Credential if (credentials.auth_type === AuthType.none) { @@ -52,6 +55,7 @@ const TestApi: FC<Props> = ({ } const res = await testAPIAvailable(data) as any setResult(res.error || res.result) + setTesting(false) } return ( @@ -107,7 +111,7 @@ const TestApi: FC<Props> = ({ </div> </div> - <Button variant='primary' className=' mt-4 h-10 w-full' onClick={handleTest}>{t('tools.test.title')}</Button> + <Button variant='primary' className=' mt-4 h-10 w-full' loading={testing} disabled={testing} onClick={handleTest}>{t('tools.test.title')}</Button> <div className='mt-6'> <div className='flex items-center space-x-3'> <div className='system-xs-semibold text-text-tertiary'>{t('tools.test.testResult')}</div> diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx index 8c805e1d5b..2e42957a0d 100644 --- a/web/app/components/tools/marketplace/index.tsx +++ b/web/app/components/tools/marketplace/index.tsx @@ -12,7 +12,7 @@ import { useMarketplace } from './hooks' import List from '@/app/components/plugins/marketplace/list' import Loading from '@/app/components/base/loading' import { getLocaleOnClient } from '@/i18n' -import { MARKETPLACE_URL_PREFIX } from '@/config' +import { getMarketplaceUrl } from '@/utils/var' type MarketplaceProps = { searchPluginText: string @@ -84,7 +84,7 @@ const Marketplace = ({ </span> {t('common.operation.in')} <a - href={`${MARKETPLACE_URL_PREFIX}?language=${locale}&q=${searchPluginText}&tags=${filterPluginTags.join(',')}${theme ? `&theme=${theme}` : ''}`} + href={getMarketplaceUrl('', { language: locale, q: searchPluginText, tags: filterPluginTags.join(','), theme })} className='system-sm-medium ml-1 flex items-center text-text-accent' target='_blank' > diff --git a/web/app/components/tools/mcp/create-card.tsx b/web/app/components/tools/mcp/create-card.tsx new file mode 100644 index 0000000000..7416f85a2f --- /dev/null +++ b/web/app/components/tools/mcp/create-card.tsx @@ -0,0 +1,75 @@ +'use client' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import { + RiAddCircleFill, + RiArrowRightUpLine, + RiBookOpenLine, +} from '@remixicon/react' +import MCPModal from './modal' +import I18n from '@/context/i18n' +import { getLanguage } from '@/i18n/language' +import { useAppContext } from '@/context/app-context' +import { useCreateMCP } from '@/service/use-tools' +import type { ToolWithProvider } from '@/app/components/workflow/types' + +type Props = { + handleCreate: (provider: ToolWithProvider) => void +} + +const NewMCPCard = ({ handleCreate }: Props) => { + const { t } = useTranslation() + const { locale } = useContext(I18n) + const language = getLanguage(locale) + const { isCurrentWorkspaceManager } = useAppContext() + + const { mutateAsync: createMCP } = useCreateMCP() + + const create = async (info: any) => { + const provider = await createMCP(info) + handleCreate(provider) + } + + const linkUrl = useMemo(() => { + if (language.startsWith('zh_')) + return 'https://docs.dify.ai/zh-hans/guides/tools/mcp' + if (language.startsWith('ja_jp')) + return 'https://docs.dify.ai/ja_jp/guides/tools/mcp' + return 'https://docs.dify.ai/en/guides/tools/mcp' + }, [language]) + + const [showModal, setShowModal] = useState(false) + + return ( + <> + {isCurrentWorkspaceManager && ( + <div className='col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'> + <div className='group grow rounded-t-xl' onClick={() => setShowModal(true)}> + <div className='flex shrink-0 items-center p-4 pb-3'> + <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'> + <RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/> + </div> + <div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.mcp.create.cardTitle')}</div> + </div> + </div> + <div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'> + <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'> + <RiBookOpenLine className='h-3 w-3 shrink-0' /> + <div className='system-xs-regular grow truncate' title={t('tools.mcp.create.cardLink') || ''}>{t('tools.mcp.create.cardLink')}</div> + <RiArrowRightUpLine className='h-3 w-3 shrink-0' /> + </a> + </div> + </div> + )} + {showModal && ( + <MCPModal + show={showModal} + onConfirm={create} + onHide={() => setShowModal(false)} + /> + )} + </> + ) +} +export default NewMCPCard diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx new file mode 100644 index 0000000000..3f927b990d --- /dev/null +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -0,0 +1,308 @@ +'use client' +import React, { useCallback, useEffect } from 'react' +import type { FC } from 'react' +import { useBoolean } from 'ahooks' +import copy from 'copy-to-clipboard' +import { useTranslation } from 'react-i18next' +import { useAppContext } from '@/context/app-context' +import { + RiCloseLine, + RiLoader2Line, + RiLoopLeftLine, +} from '@remixicon/react' +import type { ToolWithProvider } from '../../../workflow/types' +import Icon from '@/app/components/plugins/card/base/card-icon' +import ActionButton from '@/app/components/base/action-button' +import Button from '@/app/components/base/button' +import Confirm from '@/app/components/base/confirm' +import Indicator from '@/app/components/header/indicator' +import Tooltip from '@/app/components/base/tooltip' +import MCPModal from '../modal' +import OperationDropdown from './operation-dropdown' +import ListLoading from './list-loading' +import ToolItem from './tool-item' +import { + useAuthorizeMCP, + useDeleteMCP, + useInvalidateMCPTools, + useMCPTools, + useUpdateMCP, + useUpdateMCPTools, +} from '@/service/use-tools' +import { openOAuthPopup } from '@/hooks/use-oauth' +import cn from '@/utils/classnames' + +type Props = { + detail: ToolWithProvider + onUpdate: (isDelete?: boolean) => void + onHide: () => void + isTriggerAuthorize: boolean + onFirstCreate: () => void +} + +const MCPDetailContent: FC<Props> = ({ + detail, + onUpdate, + onHide, + isTriggerAuthorize, + onFirstCreate, +}) => { + const { t } = useTranslation() + const { isCurrentWorkspaceManager } = useAppContext() + + const { data, isFetching: isGettingTools } = useMCPTools(detail.is_team_authorization ? detail.id : '') + const invalidateMCPTools = useInvalidateMCPTools() + const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() + const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() + const toolList = data?.tools || [] + + const [isShowUpdateConfirm, { + setTrue: showUpdateConfirm, + setFalse: hideUpdateConfirm, + }] = useBoolean(false) + + const handleUpdateTools = useCallback(async () => { + hideUpdateConfirm() + if (!detail) + return + await updateTools(detail.id) + invalidateMCPTools(detail.id) + onUpdate() + }, [detail, hideUpdateConfirm, invalidateMCPTools, onUpdate, updateTools]) + + const { mutateAsync: updateMCP } = useUpdateMCP({}) + const { mutateAsync: deleteMCP } = useDeleteMCP({}) + + const [isShowUpdateModal, { + setTrue: showUpdateModal, + setFalse: hideUpdateModal, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleOAuthCallback = useCallback(() => { + if (!isCurrentWorkspaceManager) + return + if (!detail.id) + return + handleUpdateTools() + }, [detail.id, handleUpdateTools, isCurrentWorkspaceManager]) + + const handleAuthorize = useCallback(async () => { + onFirstCreate() + if (!isCurrentWorkspaceManager) + return + if (!detail) + return + const res = await authorizeMcp({ + provider_id: detail.id, + }) + if (res.result === 'success') + handleUpdateTools() + + else if (res.authorization_url) + openOAuthPopup(res.authorization_url, handleOAuthCallback) + }, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback]) + + const handleUpdate = useCallback(async (data: any) => { + if (!detail) + return + const res = await updateMCP({ + ...data, + provider_id: detail.id, + }) + if ((res as any)?.result === 'success') { + hideUpdateModal() + onUpdate() + handleAuthorize() + } + }, [detail, updateMCP, hideUpdateModal, onUpdate, handleAuthorize]) + + const handleDelete = useCallback(async () => { + if (!detail) + return + showDeleting() + const res = await deleteMCP(detail.id) + hideDeleting() + if ((res as any)?.result === 'success') { + hideDeleteConfirm() + onUpdate(true) + } + }, [detail, showDeleting, deleteMCP, hideDeleting, hideDeleteConfirm, onUpdate]) + + useEffect(() => { + if (isTriggerAuthorize) + handleAuthorize() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + if (!detail) + return null + + return ( + <> + <div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}> + <div className='flex'> + <div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'> + <Icon src={detail.icon} /> + </div> + <div className='ml-3 w-0 grow'> + <div className='flex h-5 items-center'> + <div className='system-md-semibold truncate text-text-primary' title={detail.name}>{detail.name}</div> + </div> + <div className='mt-0.5 flex items-center gap-1'> + <Tooltip popupContent={t('tools.mcp.identifier')}> + <div className='system-xs-regular shrink-0 cursor-pointer text-text-secondary' onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div> + </Tooltip> + <div className='system-xs-regular shrink-0 text-text-quaternary'>·</div> + <Tooltip popupContent={t('tools.mcp.modal.serverUrl')}> + <div className='system-xs-regular truncate text-text-secondary'>{detail.server_url}</div> + </Tooltip> + </div> + </div> + <div className='flex gap-1'> + <OperationDropdown + onEdit={showUpdateModal} + onRemove={showDeleteConfirm} + /> + <ActionButton onClick={onHide}> + <RiCloseLine className='h-4 w-4' /> + </ActionButton> + </div> + </div> + <div className='mt-5'> + {!isAuthorizing && detail.is_team_authorization && ( + <Button + variant='secondary' + className='w-full' + onClick={handleAuthorize} + disabled={!isCurrentWorkspaceManager} + > + <Indicator className='mr-2' color={'green'} /> + {t('tools.auth.authorized')} + </Button> + )} + {!detail.is_team_authorization && !isAuthorizing && ( + <Button + variant='primary' + className='w-full' + onClick={handleAuthorize} + disabled={!isCurrentWorkspaceManager} + > + {t('tools.mcp.authorize')} + </Button> + )} + {isAuthorizing && ( + <Button + variant='primary' + className='w-full' + disabled + > + <RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} /> + {t('tools.mcp.authorizing')} + </Button> + )} + </div> + </div> + <div className='flex grow flex-col'> + {((detail.is_team_authorization && isGettingTools) || isUpdating) && ( + <> + <div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'> + <div className='flex h-6 items-center'> + {!isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.gettingTools')}</div>} + {isUpdating && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.updateTools')}</div>} + </div> + <div></div> + </div> + <div className='flex h-full w-full grow flex-col overflow-hidden px-4 pb-4'> + <ListLoading /> + </div> + </> + )} + {!isUpdating && detail.is_team_authorization && !isGettingTools && !toolList.length && ( + <div className='flex h-full w-full flex-col items-center justify-center'> + <div className='system-sm-regular mb-3 text-text-tertiary'>{t('tools.mcp.toolsEmpty')}</div> + <Button + variant='primary' + onClick={handleUpdateTools} + >{t('tools.mcp.getTools')}</Button> + </div> + )} + {!isUpdating && !isGettingTools && toolList.length > 0 && ( + <> + <div className='flex shrink-0 justify-between gap-2 px-4 pb-1 pt-2'> + <div className='flex h-6 items-center'> + {toolList.length > 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.toolsNum', { count: toolList.length })}</div>} + {toolList.length === 1 && <div className='system-sm-semibold-uppercase text-text-secondary'>{t('tools.mcp.onlyTool')}</div>} + </div> + <div> + <Button size='small' onClick={showUpdateConfirm}> + <RiLoopLeftLine className='mr-1 h-3.5 w-3.5' /> + {t('tools.mcp.update')} + </Button> + </div> + </div> + <div className='flex h-0 w-full grow flex-col gap-2 overflow-y-auto px-4 pb-4'> + {toolList.map(tool => ( + <ToolItem + key={`${detail.id}${tool.name}`} + tool={tool} + /> + ))} + </div> + </> + )} + + {!isUpdating && !detail.is_team_authorization && ( + <div className='flex h-full w-full flex-col items-center justify-center'> + {!isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizingRequired')}</div>} + {isAuthorizing && <div className='system-md-medium mb-1 text-text-secondary'>{t('tools.mcp.authorizing')}</div>} + <div className='system-sm-regular text-text-tertiary'>{t('tools.mcp.authorizeTip')}</div> + </div> + )} + </div> + {isShowUpdateModal && ( + <MCPModal + data={detail} + show={isShowUpdateModal} + onConfirm={handleUpdate} + onHide={hideUpdateModal} + /> + )} + {isShowDeleteConfirm && ( + <Confirm + isShow + title={t('tools.mcp.delete')} + content={ + <div> + {t('tools.mcp.deleteConfirmTitle', { mcp: detail.name })} + </div> + } + onCancel={hideDeleteConfirm} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} + /> + )} + {isShowUpdateConfirm && ( + <Confirm + isShow + title={t('tools.mcp.toolUpdateConfirmTitle')} + content={t('tools.mcp.toolUpdateConfirmContent')} + onCancel={hideUpdateConfirm} + onConfirm={handleUpdateTools} + /> + )} + </> + ) +} + +export default MCPDetailContent diff --git a/web/app/components/tools/mcp/detail/list-loading.tsx b/web/app/components/tools/mcp/detail/list-loading.tsx new file mode 100644 index 0000000000..babf050d8b --- /dev/null +++ b/web/app/components/tools/mcp/detail/list-loading.tsx @@ -0,0 +1,37 @@ +'use client' +import React from 'react' +import cn from '@/utils/classnames' + +const ListLoading = () => { + return ( + <div className={cn('space-y-2')}> + <div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'> + <div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div> + <div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div> + <div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div> + </div> + <div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'> + <div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div> + <div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div> + <div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div> + </div> + <div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'> + <div className='h-2 w-[196px] rounded-sm bg-text-quaternary opacity-20'></div> + <div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div> + <div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div> + </div> + <div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'> + <div className='h-2 w-[148px] rounded-sm bg-text-quaternary opacity-20'></div> + <div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div> + <div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div> + </div> + <div className='space-y-3 rounded-xl bg-components-panel-on-panel-item-bg-hover p-4'> + <div className='h-2 w-[180px] rounded-sm bg-text-quaternary opacity-20'></div> + <div className='h-2 rounded-sm bg-text-quaternary opacity-10'></div> + <div className='mr-10 h-2 rounded-sm bg-text-quaternary opacity-10'></div> + </div> + </div> + ) +} + +export default ListLoading diff --git a/web/app/components/tools/mcp/detail/operation-dropdown.tsx b/web/app/components/tools/mcp/detail/operation-dropdown.tsx new file mode 100644 index 0000000000..d2cbc8825d --- /dev/null +++ b/web/app/components/tools/mcp/detail/operation-dropdown.tsx @@ -0,0 +1,88 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiDeleteBinLine, + RiEditLine, + RiMoreFill, +} from '@remixicon/react' +import ActionButton from '@/app/components/base/action-button' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import cn from '@/utils/classnames' + +type Props = { + inCard?: boolean + onOpenChange?: (open: boolean) => void + onEdit: () => void + onRemove: () => void +} + +const OperationDropdown: FC<Props> = ({ + inCard, + onOpenChange, + onEdit, + onRemove, +}) => { + const { t } = useTranslation() + const [open, doSetOpen] = useState(false) + const openRef = useRef(open) + const setOpen = useCallback((v: boolean) => { + doSetOpen(v) + openRef.current = v + onOpenChange?.(v) + }, [doSetOpen]) + + const handleTrigger = useCallback(() => { + setOpen(!openRef.current) + }, [setOpen]) + + return ( + <PortalToFollowElem + open={open} + onOpenChange={setOpen} + placement='bottom-end' + offset={{ + mainAxis: !inCard ? -12 : 0, + crossAxis: !inCard ? 36 : 0, + }} + > + <PortalToFollowElemTrigger onClick={handleTrigger}> + <div> + <ActionButton size={inCard ? 'l' : 'm'} className={cn(open && 'bg-state-base-hover')}> + <RiMoreFill className={cn('h-4 w-4', inCard && 'h-5 w-5')} /> + </ActionButton> + </div> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-50'> + <div className='w-[160px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-sm'> + <div + className='flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-base-hover' + onClick={() => { + onEdit() + handleTrigger() + }} + > + <RiEditLine className='h-4 w-4 text-text-tertiary' /> + <div className='system-md-regular ml-2 text-text-secondary'>{t('tools.mcp.operation.edit')}</div> + </div> + <div + className='group flex cursor-pointer items-center rounded-lg px-3 py-1.5 hover:bg-state-destructive-hover' + onClick={() => { + onRemove() + handleTrigger() + }} + > + <RiDeleteBinLine className='h-4 w-4 text-text-tertiary group-hover:text-text-destructive-secondary' /> + <div className='system-md-regular ml-2 text-text-secondary group-hover:text-text-destructive'>{t('tools.mcp.operation.remove')}</div> + </div> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + ) +} +export default React.memo(OperationDropdown) diff --git a/web/app/components/tools/mcp/detail/provider-detail.tsx b/web/app/components/tools/mcp/detail/provider-detail.tsx new file mode 100644 index 0000000000..56f26f8582 --- /dev/null +++ b/web/app/components/tools/mcp/detail/provider-detail.tsx @@ -0,0 +1,56 @@ +'use client' +import React from 'react' +import type { FC } from 'react' +import Drawer from '@/app/components/base/drawer' +import MCPDetailContent from './content' +import type { ToolWithProvider } from '../../../workflow/types' +import cn from '@/utils/classnames' + +type Props = { + detail?: ToolWithProvider + onUpdate: () => void + onHide: () => void + isTriggerAuthorize: boolean + onFirstCreate: () => void +} + +const MCPDetailPanel: FC<Props> = ({ + detail, + onUpdate, + onHide, + isTriggerAuthorize, + onFirstCreate, +}) => { + const handleUpdate = (isDelete = false) => { + if (isDelete) + onHide() + onUpdate() + } + + if (!detail) + return null + + return ( + <Drawer + isOpen={!!detail} + clickOutsideNotOpen={false} + onClose={onHide} + footer={null} + mask={false} + positionCenter={false} + panelClassName={cn('mb-2 mr-2 mt-[64px] !w-[420px] !max-w-[420px] justify-start rounded-2xl border-[0.5px] border-components-panel-border !bg-components-panel-bg !p-0 shadow-xl')} + > + {detail && ( + <MCPDetailContent + detail={detail} + onHide={onHide} + onUpdate={handleUpdate} + isTriggerAuthorize={isTriggerAuthorize} + onFirstCreate={onFirstCreate} + /> + )} + </Drawer> + ) +} + +export default MCPDetailPanel diff --git a/web/app/components/tools/mcp/detail/tool-item.tsx b/web/app/components/tools/mcp/detail/tool-item.tsx new file mode 100644 index 0000000000..dec82edcca --- /dev/null +++ b/web/app/components/tools/mcp/detail/tool-item.tsx @@ -0,0 +1,41 @@ +'use client' +import React from 'react' +import { useContext } from 'use-context-selector' +import type { Tool } from '@/app/components/tools/types' +import I18n from '@/context/i18n' +import { getLanguage } from '@/i18n/language' +import Tooltip from '@/app/components/base/tooltip' +import cn from '@/utils/classnames' + +type Props = { + tool: Tool +} + +const MCPToolItem = ({ + tool, +}: Props) => { + const { locale } = useContext(I18n) + const language = getLanguage(locale) + + return ( + <Tooltip + key={tool.name} + position='left' + popupClassName='!p-0 !px-4 !py-3.5 !w-[360px] !border-[0.5px] !border-components-panel-border !rounded-xl !shadow-lg' + popupContent={( + <div> + <div className='title-xs-semi-bold mb-1 text-text-primary'>{tool.label[language]}</div> + <div className='body-xs-regular text-text-secondary'>{tool.description[language]}</div> + </div> + )} + > + <div + className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover')} + > + <div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div> + <div className='system-xs-regular line-clamp-2 text-text-tertiary' title={tool.description[language]}>{tool.description[language]}</div> + </div> + </Tooltip> + ) +} +export default MCPToolItem diff --git a/web/app/components/tools/mcp/hooks.ts b/web/app/components/tools/mcp/hooks.ts new file mode 100644 index 0000000000..b2b521557f --- /dev/null +++ b/web/app/components/tools/mcp/hooks.ts @@ -0,0 +1,12 @@ +import dayjs from 'dayjs' +import { useCallback } from 'react' +import { useI18N } from '@/context/i18n' + +export const useFormatTimeFromNow = () => { + const { locale } = useI18N() + const formatTimeFromNow = useCallback((time: number) => { + return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() + }, [locale]) + + return { formatTimeFromNow } +} diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx new file mode 100644 index 0000000000..5a1e5cf3bf --- /dev/null +++ b/web/app/components/tools/mcp/index.tsx @@ -0,0 +1,98 @@ +'use client' +import { useMemo, useState } from 'react' +import NewMCPCard from './create-card' +import MCPCard from './provider-card' +import MCPDetailPanel from './detail/provider-detail' +import { + useAllToolProviders, +} from '@/service/use-tools' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type Props = { + searchText: string +} + +function renderDefaultCard() { + const defaultCards = Array.from({ length: 36 }, (_, index) => ( + <div + key={index} + className={cn( + 'inline-flex h-[111px] rounded-xl bg-background-default-lighter opacity-10', + index < 4 && 'opacity-60', + index >= 4 && index < 8 && 'opacity-50', + index >= 8 && index < 12 && 'opacity-40', + index >= 12 && index < 16 && 'opacity-30', + index >= 16 && index < 20 && 'opacity-25', + index >= 20 && index < 24 && 'opacity-20', + )} + ></div> + )) + return defaultCards +} + +const MCPList = ({ + searchText, +}: Props) => { + const { data: list = [] as ToolWithProvider[], refetch } = useAllToolProviders() + const [isTriggerAuthorize, setIsTriggerAuthorize] = useState<boolean>(false) + + const filteredList = useMemo(() => { + return list.filter((collection) => { + if (searchText) + return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase())) + return collection.type === 'mcp' + }) as ToolWithProvider[] + }, [list, searchText]) + + const [currentProviderID, setCurrentProviderID] = useState<string>() + + const currentProvider = useMemo(() => { + return list.find(provider => provider.id === currentProviderID) + }, [list, currentProviderID]) + + const handleCreate = async (provider: ToolWithProvider) => { + await refetch() // update list + setCurrentProviderID(provider.id) + setIsTriggerAuthorize(true) + } + + const handleUpdate = async (providerID: string) => { + await refetch() // update list + setCurrentProviderID(providerID) + setIsTriggerAuthorize(true) + } + return ( + <> + <div + className={cn( + 'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6', + !list.length && 'h-[calc(100vh_-_136px)] overflow-hidden', + )} + > + <NewMCPCard handleCreate={handleCreate} /> + {filteredList.map(provider => ( + <MCPCard + key={provider.id} + data={provider} + currentProvider={currentProvider as ToolWithProvider} + handleSelect={setCurrentProviderID} + onUpdate={handleUpdate} + onDeleted={refetch} + /> + ))} + {!list.length && renderDefaultCard()} + </div> + {currentProvider && ( + <MCPDetailPanel + detail={currentProvider as ToolWithProvider} + onHide={() => setCurrentProviderID(undefined)} + onUpdate={refetch} + isTriggerAuthorize={isTriggerAuthorize} + onFirstCreate={() => setIsTriggerAuthorize(false)} + /> + )} + </> + ) +} +export default MCPList diff --git a/web/app/components/tools/mcp/mcp-server-modal.tsx b/web/app/components/tools/mcp/mcp-server-modal.tsx new file mode 100644 index 0000000000..9eb33f21ec --- /dev/null +++ b/web/app/components/tools/mcp/mcp-server-modal.tsx @@ -0,0 +1,134 @@ +'use client' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Textarea from '@/app/components/base/textarea' +import Divider from '@/app/components/base/divider' +import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item' +import type { + MCPServerDetail, +} from '@/app/components/tools/types' +import { + useCreateMCPServer, + useInvalidateMCPServerDetail, + useUpdateMCPServer, +} from '@/service/use-tools' +import cn from '@/utils/classnames' + +export type ModalProps = { + appID: string + latestParams?: any[] + data?: MCPServerDetail + show: boolean + onHide: () => void +} + +const MCPServerModal = ({ + appID, + latestParams = [], + data, + show, + onHide, +}: ModalProps) => { + const { t } = useTranslation() + const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer() + const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer() + const invalidateMCPServerDetail = useInvalidateMCPServerDetail() + + const [description, setDescription] = React.useState(data?.description || '') + const [params, setParams] = React.useState(data?.parameters || {}) + + const handleParamChange = (variable: string, value: string) => { + setParams(prev => ({ + ...prev, + [variable]: value, + })) + } + + const getParamValue = () => { + const res = {} as any + latestParams.map((param) => { + res[param.variable] = params[param.variable] + return param + }) + return res + } + + const submit = async () => { + if (!data) { + await createMCPServer({ + appID, + description, + parameters: getParamValue(), + }) + invalidateMCPServerDetail(appID) + onHide() + } + else { + await updateMCPServer({ + appID, + id: data.id, + description, + parameters: getParamValue(), + }) + invalidateMCPServerDetail(appID) + onHide() + } + } + + return ( + <Modal + isShow={show} + onClose={onHide} + className={cn('relative !max-w-[520px] !p-0')} + > + <div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}> + <RiCloseLine className='h-5 w-5 text-text-tertiary' /> + </div> + <div className='title-2xl-semi-bold relative p-6 pb-3 text-xl text-text-primary'> + {!data ? t('tools.mcp.server.modal.addTitle') : t('tools.mcp.server.modal.editTitle')} + </div> + <div className='space-y-5 px-6 py-3'> + <div className='space-y-0.5'> + <div className='flex h-6 items-center gap-1'> + <div className='system-sm-medium text-text-secondary'>{t('tools.mcp.server.modal.description')}</div> + <div className='system-xs-regular text-text-destructive-secondary'>*</div> + </div> + <Textarea + className='h-[96px] resize-none' + value={description} + placeholder={t('tools.mcp.server.modal.descriptionPlaceholder')} + onChange={e => setDescription(e.target.value)} + ></Textarea> + </div> + {latestParams.length > 0 && ( + <div> + <div className='mb-1 flex items-center gap-2'> + <div className='system-xs-medium-uppercase shrink-0 text-text-primary'>{t('tools.mcp.server.modal.parameters')}</div> + <Divider type='horizontal' className='!m-0 !h-px grow bg-divider-subtle' /> + </div> + <div className='body-xs-regular mb-2 text-text-tertiary'>{t('tools.mcp.server.modal.parametersTip')}</div> + <div className='space-y-3'> + {latestParams.map(paramItem => ( + <MCPServerParamItem + key={paramItem.variable} + data={paramItem} + value={params[paramItem.variable] || ''} + onChange={value => handleParamChange(paramItem.variable, value)} + /> + ))} + </div> + </div> + )} + </div> + <div className='flex flex-row-reverse p-6 pt-5'> + <Button disabled={!description || creating || updating} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.server.modal.confirm')}</Button> + <Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button> + </div> + </Modal> + ) +} + +export default MCPServerModal diff --git a/web/app/components/tools/mcp/mcp-server-param-item.tsx b/web/app/components/tools/mcp/mcp-server-param-item.tsx new file mode 100644 index 0000000000..a48d1b92b0 --- /dev/null +++ b/web/app/components/tools/mcp/mcp-server-param-item.tsx @@ -0,0 +1,37 @@ +'use client' +import React from 'react' +import { useTranslation } from 'react-i18next' +import Textarea from '@/app/components/base/textarea' + +type Props = { + data?: any + value: string + onChange: (value: string) => void +} + +const MCPServerParamItem = ({ + data, + value, + onChange, +}: Props) => { + const { t } = useTranslation() + + return ( + <div className='space-y-0.5'> + <div className='flex h-6 items-center gap-2'> + <div className='system-xs-medium text-text-secondary'>{data.label}</div> + <div className='system-xs-medium text-text-quaternary'>·</div> + <div className='system-xs-medium text-text-secondary'>{data.variable}</div> + <div className='system-xs-medium text-text-tertiary'>{data.type}</div> + </div> + <Textarea + className='h-8 resize-none' + value={value} + placeholder={t('tools.mcp.server.modal.parametersPlaceholder')} + onChange={e => onChange(e.target.value)} + ></Textarea> + </div> + ) +} + +export default MCPServerParamItem diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx new file mode 100644 index 0000000000..c0c542da26 --- /dev/null +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -0,0 +1,246 @@ +'use client' +import React, { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiEditLine, RiLoopLeftLine } from '@remixicon/react' +import { + Mcp, +} from '@/app/components/base/icons/src/vender/other' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' +import Switch from '@/app/components/base/switch' +import Divider from '@/app/components/base/divider' +import CopyFeedback from '@/app/components/base/copy-feedback' +import Confirm from '@/app/components/base/confirm' +import type { AppDetailResponse } from '@/models/app' +import { useAppContext } from '@/context/app-context' +import type { AppSSO } from '@/types/app' +import Indicator from '@/app/components/header/indicator' +import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' +import { useAppWorkflow } from '@/service/use-workflow' +import { + useInvalidateMCPServerDetail, + useMCPServerDetail, + useRefreshMCPServerCode, + useUpdateMCPServer, +} from '@/service/use-tools' +import { BlockEnum } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +import { fetchAppDetail } from '@/service/apps' + +export type IAppCardProps = { + appInfo: AppDetailResponse & Partial<AppSSO> +} + +function MCPServiceCard({ + appInfo, +}: IAppCardProps) { + const { t } = useTranslation() + const appId = appInfo.id + const { mutateAsync: updateMCPServer } = useUpdateMCPServer() + const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode() + const invalidateMCPServerDetail = useInvalidateMCPServerDetail() + const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext() + const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [showMCPServerModal, setShowMCPServerModal] = useState(false) + + const isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow' + const isBasicApp = !isAdvancedApp + const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '') + const [basicAppConfig, setBasicAppConfig] = useState<any>({}) + const basicAppInputForm = useMemo(() => { + if(!isBasicApp || !basicAppConfig?.user_input_form) + return [] + return basicAppConfig.user_input_form.map((item: any) => { + const type = Object.keys(item)[0] + return { + ...item[type], + type: type || 'text-input', + } + }) + }, [basicAppConfig.user_input_form, isBasicApp]) + useEffect(() => { + if(isBasicApp && appId) { + (async () => { + const res = await fetchAppDetail({ url: '/apps', id: appId }) + setBasicAppConfig(res?.model_config || {}) + })() + } + }, [appId, isBasicApp]) + const { data: detail } = useMCPServerDetail(appId) + const { id, status, server_code } = detail ?? {} + + const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at + const serverPublished = !!id + const serverActivated = status === 'active' + const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********' + const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished + + const [activated, setActivated] = useState(serverActivated) + + const latestParams = useMemo(() => { + if(isAdvancedApp) { + if (!currentWorkflow?.graph) + return [] + const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any + return startNode?.data.variables as any[] || [] + } + return basicAppInputForm + }, [currentWorkflow, basicAppInputForm, isAdvancedApp]) + + const onGenCode = async () => { + await refreshMCPServerCode(detail?.id || '') + invalidateMCPServerDetail(appId) + } + + const onChangeStatus = async (state: boolean) => { + setActivated(state) + if (state) { + if (!serverPublished) { + setShowMCPServerModal(true) + return + } + + await updateMCPServer({ + appID: appId, + id: id || '', + description: detail?.description || '', + parameters: detail?.parameters || {}, + status: 'active', + }) + invalidateMCPServerDetail(appId) + } + else { + await updateMCPServer({ + appID: appId, + id: id || '', + description: detail?.description || '', + parameters: detail?.parameters || {}, + status: 'inactive', + }) + invalidateMCPServerDetail(appId) + } + } + + const handleServerModalHide = () => { + setShowMCPServerModal(false) + if (!serverActivated) + setActivated(false) + } + + useEffect(() => { + setActivated(serverActivated) + }, [serverActivated]) + + if (!currentWorkflow && isAdvancedApp) + return null + + return ( + <> + <div className={cn('w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight')}> + <div className='rounded-xl bg-background-default'> + <div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'> + <div className='flex w-full items-center gap-3 self-stretch'> + <div className='flex grow items-center'> + <div className='mr-3 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-indigo-indigo-500 p-1 shadow-md'> + <Mcp className='h-4 w-4 text-text-primary-on-surface' /> + </div> + <div className="group w-full"> + <div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary"> + {t('tools.mcp.server.title')} + </div> + </div> + </div> + <div className='flex items-center gap-1'> + <Indicator color={serverActivated ? 'green' : 'yellow'} /> + <div className={`${serverActivated ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase`}> + {serverActivated + ? t('appOverview.overview.status.running') + : t('appOverview.overview.status.disable')} + </div> + </div> + <Tooltip + popupContent={appUnpublished ? t('tools.mcp.server.publishTip') : ''} + > + <div> + <Switch defaultValue={activated} onChange={onChangeStatus} disabled={toggleDisabled} /> + </div> + </Tooltip> + </div> + <div className='flex flex-col items-start justify-center self-stretch'> + <div className="system-xs-medium pb-1 text-text-tertiary"> + {t('tools.mcp.server.url')} + </div> + <div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2"> + <div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1"> + <div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary"> + {serverURL} + </div> + </div> + {serverPublished && ( + <> + <CopyFeedback + content={serverURL} + className={'!size-6'} + /> + <Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" /> + {isCurrentWorkspaceManager && ( + <Tooltip + popupContent={t('appOverview.overview.appInfo.regenerate') || ''} + > + <div + className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover" + onClick={() => setShowConfirmDelete(true)} + > + <RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')}/> + </div> + </Tooltip> + )} + </> + )} + </div> + </div> + </div> + <div className='flex items-center gap-1 self-stretch p-3'> + <Button + disabled={toggleDisabled} + size='small' + variant='ghost' + onClick={() => setShowMCPServerModal(true)} + > + + <div className="flex items-center justify-center gap-[1px]"> + <RiEditLine className="h-3.5 w-3.5" /> + <div className="system-xs-medium px-[3px] text-text-tertiary">{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}</div> + </div> + </Button> + </div> + </div> + </div> + {showMCPServerModal && ( + <MCPServerModal + show={showMCPServerModal} + appID={appId} + data={serverPublished ? detail : undefined} + latestParams={latestParams} + onHide={handleServerModalHide} + /> + )} + {/* button copy link/ button regenerate */} + {showConfirmDelete && ( + <Confirm + type='warning' + title={t('appOverview.overview.appInfo.regenerate')} + content={t('tools.mcp.server.reGen')} + isShow={showConfirmDelete} + onConfirm={() => { + onGenCode() + setShowConfirmDelete(false) + }} + onCancel={() => setShowConfirmDelete(false)} + /> + )} + </> + ) +} + +export default MCPServiceCard diff --git a/web/app/components/tools/mcp/mock.ts b/web/app/components/tools/mcp/mock.ts new file mode 100644 index 0000000000..f271f67ed3 --- /dev/null +++ b/web/app/components/tools/mcp/mock.ts @@ -0,0 +1,154 @@ +const tools = [ + { + author: 'Novice', + name: 'NOTION_ADD_PAGE_CONTENT', + label: { + en_US: 'NOTION_ADD_PAGE_CONTENT', + zh_Hans: 'NOTION_ADD_PAGE_CONTENT', + pt_BR: 'NOTION_ADD_PAGE_CONTENT', + ja_JP: 'NOTION_ADD_PAGE_CONTENT', + }, + description: { + en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + }, + parameters: [ + { + name: 'after', + label: { + en_US: 'after', + zh_Hans: 'after', + pt_BR: 'after', + ja_JP: 'after', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + }, + form: 'llm', + llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + }, + { + name: 'content_block', + label: { + en_US: 'content_block', + zh_Hans: 'content_block', + pt_BR: 'content_block', + ja_JP: 'content_block', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'Child content to append to a page.', + zh_Hans: 'Child content to append to a page.', + pt_BR: 'Child content to append to a page.', + ja_JP: 'Child content to append to a page.', + }, + form: 'llm', + llm_description: 'Child content to append to a page.', + }, + { + name: 'parent_block_id', + label: { + en_US: 'parent_block_id', + zh_Hans: 'parent_block_id', + pt_BR: 'parent_block_id', + ja_JP: 'parent_block_id', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'The ID of the page which the children will be added.', + zh_Hans: 'The ID of the page which the children will be added.', + pt_BR: 'The ID of the page which the children will be added.', + ja_JP: 'The ID of the page which the children will be added.', + }, + form: 'llm', + llm_description: 'The ID of the page which the children will be added.', + }, + ], + labels: [], + output_schema: null, + }, +] + +export const listData = [ + { + id: 'fdjklajfkljadslf111', + author: 'KVOJJJin', + name: 'GOGOGO', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: true, + tools, + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO', + zh_Hans: 'GOGOGO', + }, + }, + { + id: 'fdjklajfkljadslf222', + author: 'KVOJJJin', + name: 'GOGOGO2', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: false, + tools: [], + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO2', + zh_Hans: 'GOGOGO2', + }, + }, + { + id: 'fdjklajfkljadslf333', + author: 'KVOJJJin', + name: 'GOGOGO3', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: true, + tools, + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO3', + zh_Hans: 'GOGOGO3', + }, + }, +] diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx new file mode 100644 index 0000000000..0e57cb149b --- /dev/null +++ b/web/app/components/tools/mcp/modal.tsx @@ -0,0 +1,221 @@ +'use client' +import React, { useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { getDomain } from 'tldts' +import { RiCloseLine, RiEditLine } from '@remixicon/react' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import AppIcon from '@/app/components/base/app-icon' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import type { AppIconType } from '@/types/app' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import { noop } from 'lodash-es' +import Toast from '@/app/components/base/toast' +import { uploadRemoteFileInfo } from '@/service/common' +import cn from '@/utils/classnames' +import { useHover } from 'ahooks' + +export type DuplicateAppModalProps = { + data?: ToolWithProvider + show: boolean + onConfirm: (info: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + server_identifier: string + }) => void + onHide: () => void +} + +const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' } +const extractFileId = (url: string) => { + const match = url.match(/files\/(.+?)\/file-preview/) + return match ? match[1] : null +} +const getIcon = (data?: ToolWithProvider) => { + if (!data) + return DEFAULT_ICON as AppIconSelection + if (typeof data.icon === 'string') + return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection + return { + ...data.icon, + icon: data.icon.content, + type: 'emoji', + } as unknown as AppIconSelection +} + +const MCPModal = ({ + data, + show, + onConfirm, + onHide, +}: DuplicateAppModalProps) => { + const { t } = useTranslation() + const isCreate = !data + + const originalServerUrl = data?.server_url + const originalServerID = data?.server_identifier + const [url, setUrl] = React.useState(data?.server_url || '') + const [name, setName] = React.useState(data?.name || '') + const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data)) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') + const [isFetchingIcon, setIsFetchingIcon] = useState(false) + const appIconRef = useRef<HTMLDivElement>(null) + const isHovering = useHover(appIconRef) + + const isValidUrl = (string: string) => { + try { + const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i + return urlPattern.test(string) + } + catch (e) { + return false + } + } + + const isValidServerID = (str: string) => { + return /^[a-z0-9_-]{1,24}$/.test(str) + } + + const handleBlur = async (url: string) => { + if (data) + return + if (!isValidUrl(url)) + return + const domain = getDomain(url) + const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128` + setIsFetchingIcon(true) + try { + const res = await uploadRemoteFileInfo(remoteIcon, undefined, true) + setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' }) + } + catch (e) { + console.error('Failed to fetch remote icon:', e) + Toast.notify({ type: 'warning', message: 'Failed to fetch remote icon' }) + } + finally { + setIsFetchingIcon(false) + } + } + + const submit = async () => { + if (!isValidUrl(url)) { + Toast.notify({ type: 'error', message: 'invalid server url' }) + return + } + if (!isValidServerID(serverIdentifier.trim())) { + Toast.notify({ type: 'error', message: 'invalid server identifier' }) + return + } + await onConfirm({ + server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), + name, + icon_type: appIcon.type, + icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, + icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, + server_identifier: serverIdentifier.trim(), + }) + if(isCreate) + onHide() + } + + return ( + <> + <Modal + isShow={show} + onClose={noop} + className={cn('relative !max-w-[520px]', 'p-6')} + > + <div className='absolute right-5 top-5 z-10 cursor-pointer p-1.5' onClick={onHide}> + <RiCloseLine className='h-5 w-5 text-text-tertiary' /> + </div> + <div className='title-2xl-semi-bold relative pb-3 text-xl text-text-primary'>{!isCreate ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}</div> + <div className='space-y-5 py-3'> + <div> + <div className='mb-1 flex h-6 items-center'> + <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverUrl')}</span> + </div> + <Input + value={url} + onChange={e => setUrl(e.target.value)} + onBlur={e => handleBlur(e.target.value.trim())} + placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} + /> + {originalServerUrl && originalServerUrl !== url && ( + <div className='mt-1 flex h-5 items-center'> + <span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverUrlWarning')}</span> + </div> + )} + </div> + <div className='flex space-x-3'> + <div className='grow pb-1'> + <div className='mb-1 flex h-6 items-center'> + <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.name')}</span> + </div> + <Input + value={name} + onChange={e => setName(e.target.value)} + placeholder={t('tools.mcp.modal.namePlaceholder')} + /> + </div> + <div className='pt-2' ref={appIconRef}> + <AppIcon + iconType={appIcon.type} + icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId} + background={appIcon.type === 'emoji' ? appIcon.background : undefined} + imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} + size='xxl' + className='relative cursor-pointer rounded-2xl' + coverElement={ + isHovering + ? (<div className='absolute inset-0 flex items-center justify-center overflow-hidden rounded-2xl bg-background-overlay-alt'> + <RiEditLine className='size-6 text-text-primary-on-surface' /> + </div>) : null + } + onClick={() => { setShowAppIconPicker(true) }} + /> + </div> + </div> + <div> + <div className='flex h-6 items-center'> + <span className='system-sm-medium text-text-secondary'>{t('tools.mcp.modal.serverIdentifier')}</span> + </div> + <div className='body-xs-regular mb-1 text-text-tertiary'>{t('tools.mcp.modal.serverIdentifierTip')}</div> + <Input + value={serverIdentifier} + onChange={e => setServerIdentifier(e.target.value)} + placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')} + /> + {originalServerID && originalServerID !== serverIdentifier && ( + <div className='mt-1 flex h-5 items-center'> + <span className='body-xs-regular text-text-warning'>{t('tools.mcp.modal.serverIdentifierWarning')}</span> + </div> + )} + </div> + </div> + <div className='flex flex-row-reverse pt-5'> + <Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button> + <Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button> + </div> + </Modal> + {showAppIconPicker && <AppIconPicker + onSelect={(payload) => { + setAppIcon(payload) + setShowAppIconPicker(false) + }} + onClose={() => { + setAppIcon(getIcon(data)) + setShowAppIconPicker(false) + }} + />} + </> + + ) +} + +export default MCPModal diff --git a/web/app/components/tools/mcp/provider-card.tsx b/web/app/components/tools/mcp/provider-card.tsx new file mode 100644 index 0000000000..677e25c533 --- /dev/null +++ b/web/app/components/tools/mcp/provider-card.tsx @@ -0,0 +1,152 @@ +'use client' +import { useCallback, useState } from 'react' +import { useBoolean } from 'ahooks' +import { useTranslation } from 'react-i18next' +import { useAppContext } from '@/context/app-context' +import { RiHammerFill } from '@remixicon/react' +import Indicator from '@/app/components/header/indicator' +import Icon from '@/app/components/plugins/card/base/card-icon' +import { useFormatTimeFromNow } from './hooks' +import type { ToolWithProvider } from '../../workflow/types' +import Confirm from '@/app/components/base/confirm' +import MCPModal from './modal' +import OperationDropdown from './detail/operation-dropdown' +import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools' +import cn from '@/utils/classnames' + +type Props = { + currentProvider?: ToolWithProvider + data: ToolWithProvider + handleSelect: (providerID: string) => void + onUpdate: (providerID: string) => void + onDeleted: () => void +} + +const MCPCard = ({ + currentProvider, + data, + onUpdate, + handleSelect, + onDeleted, +}: Props) => { + const { t } = useTranslation() + const { formatTimeFromNow } = useFormatTimeFromNow() + const { isCurrentWorkspaceManager } = useAppContext() + + const { mutateAsync: updateMCP } = useUpdateMCP({}) + const { mutateAsync: deleteMCP } = useDeleteMCP({}) + + const [isOperationShow, setIsOperationShow] = useState(false) + + const [isShowUpdateModal, { + setTrue: showUpdateModal, + setFalse: hideUpdateModal, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleUpdate = useCallback(async (form: any) => { + const res = await updateMCP({ + ...form, + provider_id: data.id, + }) + if ((res as any)?.result === 'success') { + hideUpdateModal() + onUpdate(data.id) + } + }, [data, updateMCP, hideUpdateModal, onUpdate]) + + const handleDelete = useCallback(async () => { + showDeleting() + const res = await deleteMCP(data.id) + hideDeleting() + if ((res as any)?.result === 'success') { + hideDeleteConfirm() + onDeleted() + } + }, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onDeleted]) + + return ( + <div + onClick={() => handleSelect(data.id)} + className={cn( + 'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md', + currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt', + )} + > + <div className='flex grow items-center gap-3 rounded-t-xl p-4'> + <div className='shrink-0 overflow-hidden rounded-xl border border-components-panel-border-subtle'> + <Icon src={data.icon} /> + </div> + <div className='grow'> + <div className='system-md-semibold mb-1 truncate text-text-secondary' title={data.name}>{data.name}</div> + <div className='system-xs-regular text-text-tertiary'>{data.server_identifier}</div> + </div> + </div> + <div className='flex items-center gap-1 rounded-b-xl pb-2.5 pl-4 pr-2.5 pt-1.5'> + <div className='flex w-0 grow items-center gap-2'> + <div className='flex items-center gap-1'> + <RiHammerFill className='h-3 w-3 shrink-0 text-text-quaternary' /> + {data.tools.length > 0 && ( + <div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.toolsCount', { count: data.tools.length })}</div> + )} + {!data.tools.length && ( + <div className='system-xs-regular shrink-0 text-text-tertiary'>{t('tools.mcp.noTools')}</div> + )} + </div> + <div className={cn('system-xs-regular text-divider-deep', (!data.is_team_authorization || !data.tools.length) && 'sm:hidden')}>/</div> + <div className={cn('system-xs-regular truncate text-text-tertiary', (!data.is_team_authorization || !data.tools.length) && ' sm:hidden')} title={`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}>{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}</div> + </div> + {data.is_team_authorization && data.tools.length > 0 && <Indicator color='green' className='shrink-0' />} + {(!data.is_team_authorization || !data.tools.length) && ( + <div className='system-xs-medium flex shrink-0 items-center gap-1 rounded-md border border-util-colors-red-red-500 bg-components-badge-bg-red-soft px-1.5 py-0.5 text-util-colors-red-red-500'> + {t('tools.mcp.noConfigured')} + <Indicator color='red' /> + </div> + )} + </div> + {isCurrentWorkspaceManager && ( + <div className={cn('absolute right-2.5 top-2.5 hidden group-hover:block', isOperationShow && 'block')} onClick={e => e.stopPropagation()}> + <OperationDropdown + inCard + onOpenChange={setIsOperationShow} + onEdit={showUpdateModal} + onRemove={showDeleteConfirm} + /> + </div> + )} + {isShowUpdateModal && ( + <MCPModal + data={data} + show={isShowUpdateModal} + onConfirm={handleUpdate} + onHide={hideUpdateModal} + /> + )} + {isShowDeleteConfirm && ( + <Confirm + isShow + title={t('tools.mcp.delete')} + content={ + <div> + {t('tools.mcp.deleteConfirmTitle', { mcp: data.name })} + </div> + } + onCancel={hideDeleteConfirm} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} + /> + )} + </div> + ) +} +export default MCPCard diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index 0970daab9c..ecfa5f6ea2 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -15,11 +15,29 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' +import MCPList from './mcp' import { useAllToolProviders } from '@/service/use-tools' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useGlobalPublicStore } from '@/context/global-public-context' +import { ToolTypeEnum } from '../workflow/block-selector/types' +const getToolType = (type: string) => { + switch (type) { + case 'builtin': + return ToolTypeEnum.BuiltIn + case 'api': + return ToolTypeEnum.Custom + case 'workflow': + return ToolTypeEnum.Workflow + case 'mcp': + return ToolTypeEnum.MCP + default: + return ToolTypeEnum.BuiltIn + } +} const ProviderList = () => { + // const searchParams = useSearchParams() + // searchParams.get('category') === 'workflow' const { t } = useTranslation() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const containerRef = useRef<HTMLDivElement>(null) @@ -31,6 +49,7 @@ const ProviderList = () => { { value: 'builtin', text: t('tools.type.builtIn') }, { value: 'api', text: t('tools.type.custom') }, { value: 'workflow', text: t('tools.type.workflow') }, + { value: 'mcp', text: 'MCP' }, ] const [tagFilterValue, setTagFilterValue] = useState<string[]>([]) const handleTagsChange = (value: string[]) => { @@ -72,7 +91,7 @@ const ProviderList = () => { className='relative flex grow flex-col overflow-y-auto bg-background-body' > <div className={cn( - 'sticky top-0 z-20 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]', + 'sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]', currentProviderId && 'pr-6', )}> <TabSliderNew @@ -85,7 +104,9 @@ const ProviderList = () => { options={options} /> <div className='flex items-center gap-2'> - <LabelFilter value={tagFilterValue} onChange={handleTagsChange} /> + {activeTab !== 'mcp' && ( + <LabelFilter value={tagFilterValue} onChange={handleTagsChange} /> + )} <Input showLeftIcon showClearIcon @@ -96,7 +117,7 @@ const ProviderList = () => { /> </div> </div> - {(filteredCollectionList.length > 0 || activeTab !== 'builtin') && ( + {activeTab !== 'mcp' && ( <div className={cn( 'relative grid shrink-0 grid-cols-1 content-start gap-4 px-12 pb-4 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4', !filteredCollectionList.length && activeTab === 'workflow' && 'grow', @@ -127,25 +148,26 @@ const ProviderList = () => { /> </div> ))} - {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty /></div>} + {!filteredCollectionList.length && activeTab === 'workflow' && <div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'><WorkflowToolEmpty type={getToolType(activeTab)} /></div>} </div> )} {!filteredCollectionList.length && activeTab === 'builtin' && ( <Empty lightCard text={t('tools.noTools')} className='h-[224px] px-12' /> )} - { - enable_marketplace && activeTab === 'builtin' && ( - <Marketplace - onMarketplaceScroll={() => { - containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) - }} - searchPluginText={keywords} - filterPluginTags={tagFilterValue} - /> - ) - } - </div > - </div > + {enable_marketplace && activeTab === 'builtin' && ( + <Marketplace + onMarketplaceScroll={() => { + containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) + }} + searchPluginText={keywords} + filterPluginTags={tagFilterValue} + /> + )} + {activeTab === 'mcp' && ( + <MCPList searchText={keywords} /> + )} + </div> + </div> {currentProvider && !currentProvider.plugin_id && ( <ProviderDetail collection={currentProvider} diff --git a/web/app/components/tools/provider/custom-create-card.tsx b/web/app/components/tools/provider/custom-create-card.tsx index 6dd268cb3a..ab5b85e260 100644 --- a/web/app/components/tools/provider/custom-create-card.tsx +++ b/web/app/components/tools/provider/custom-create-card.tsx @@ -3,17 +3,18 @@ import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { - RiAddLine, + RiAddCircleFill, + RiArrowRightUpLine, + RiBookOpenLine, } from '@remixicon/react' import type { CustomCollectionBackend } from '../types' import I18n from '@/context/i18n' import { getLanguage } from '@/i18n/language' -import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' -import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { createCustomCollection } from '@/service/tools' import Toast from '@/app/components/base/toast' import { useAppContext } from '@/context/app-context' +import { useDocLink } from '@/context/i18n' type Props = { onRefreshData: () => void @@ -25,10 +26,11 @@ const Contribute = ({ onRefreshData }: Props) => { const language = getLanguage(locale) const { isCurrentWorkspaceManager } = useAppContext() + const docLink = useDocLink() const linkUrl = useMemo(() => { - if (language.startsWith('zh_')) - return 'https://docs.dify.ai/zh-hans/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju' - return 'https://docs.dify.ai/en/guides/tools#how-to-create-custom-tools' + return docLink('/guides/tools#how-to-create-custom-tools', { + 'zh-Hans': '/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju', + }) }, [language]) const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false) @@ -45,20 +47,20 @@ const Contribute = ({ onRefreshData }: Props) => { return ( <> {isCurrentWorkspaceManager && ( - <div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-on-panel-item-bg transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg'> - <div className='group grow rounded-t-xl hover:bg-background-body' onClick={() => setIsShowEditCustomCollectionModal(true)}> + <div className='col-span-1 flex min-h-[135px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out'> + <div className='group grow rounded-t-xl' onClick={() => setIsShowEditCustomCollectionModal(true)}> <div className='flex shrink-0 items-center p-4 pb-3'> - <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg group-hover:border-components-option-card-option-border-hover group-hover:bg-components-option-card-option-bg-hover'> - <RiAddLine className='h-4 w-4 text-text-tertiary group-hover:text-text-accent'/> + <div className='flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'> + <RiAddCircleFill className='h-4 w-4 text-text-quaternary group-hover:text-text-accent'/> </div> - <div className='ml-3 text-sm font-semibold leading-5 text-text-primary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div> + <div className='system-md-semibold ml-3 text-text-secondary group-hover:text-text-accent'>{t('tools.createCustomTool')}</div> </div> </div> - <div className='rounded-b-xl border-t-[0.5px] border-divider-regular px-4 py-3 text-text-tertiary hover:bg-background-body hover:text-text-accent'> + <div className='rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent'> <a href={linkUrl} target='_blank' rel='noopener noreferrer' className='flex items-center space-x-1'> - <BookOpen01 className='h-3 w-3 shrink-0' /> - <div className='grow truncate text-xs font-normal leading-[18px]' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> - <ArrowUpRight className='h-3 w-3 shrink-0' /> + <RiBookOpenLine className='h-3 w-3 shrink-0' /> + <div className='system-xs-regular grow truncate' title={t('tools.customToolTip') || ''}>{t('tools.customToolTip')}</div> + <RiArrowRightUpLine className='h-3 w-3 shrink-0' /> </a> </div> </div> diff --git a/web/app/components/tools/provider/tool-item.tsx b/web/app/components/tools/provider/tool-item.tsx index 161b62963b..d79d20cb9c 100644 --- a/web/app/components/tools/provider/tool-item.tsx +++ b/web/app/components/tools/provider/tool-item.tsx @@ -29,7 +29,7 @@ const ToolItem = ({ return ( <> <div - className={cn('bg-components-panel-item-bg mb-2 cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')} + className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', disabled && '!cursor-not-allowed opacity-50')} onClick={() => !disabled && setShowDetail(true)} > <div className='system-md-semibold pb-0.5 text-text-secondary'>{tool.label[language]}</div> diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 32c468cde8..b83919ad18 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -7,7 +7,8 @@ export enum LOC { export enum AuthType { none = 'none', - apiKey = 'api_key', + apiKeyHeader = 'api_key_header', + apiKeyQuery = 'api_key_query', } export enum AuthHeaderPrefix { @@ -21,6 +22,7 @@ export type Credential = { api_key_header?: string api_key_value?: string api_key_header_prefix?: AuthHeaderPrefix + api_key_query_param?: string } export enum CollectionType { @@ -29,6 +31,7 @@ export enum CollectionType { custom = 'api', model = 'model', workflow = 'workflow', + mcp = 'mcp', } export type Emoji = { @@ -50,6 +53,10 @@ export type Collection = { labels: string[] plugin_id?: string letter?: string + // MCP Server + server_url?: string + updated_at?: number + server_identifier?: string } export type ToolParameter = { @@ -168,3 +175,11 @@ export type WorkflowToolProviderResponse = { } privacy_policy: string } + +export type MCPServerDetail = { + id: string + server_code: string + description: string + status: string + parameters?: Record<string, string> +} diff --git a/web/app/components/tools/utils/to-form-schema.ts b/web/app/components/tools/utils/to-form-schema.ts index 179f59021e..ee7f3379ad 100644 --- a/web/app/components/tools/utils/to-form-schema.ts +++ b/web/app/components/tools/utils/to-form-schema.ts @@ -1,4 +1,7 @@ import type { ToolCredential, ToolParameter } from '../types' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' + export const toType = (type: string) => { switch (type) { case 'string': @@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => { return formSchemas } -export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[]) => { +export const addDefaultValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => { const newValues = { ...value } formSchemas.forEach((formSchema) => { const itemValue = value[formSchema.variable] @@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record<string, any>, formSchemas: { varia return newValues } -export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any }[], isReasoning = false) => { +const correctInitialData = (type: string, target: any, defaultValue: any) => { + if (type === 'text-input' || type === 'secret-input') + target.type = 'mixed' + + if (type === 'boolean') { + if (typeof defaultValue === 'string') + target.value = defaultValue === 'true' || defaultValue === '1' + + if (typeof defaultValue === 'boolean') + target.value = defaultValue + + if (typeof defaultValue === 'number') + target.value = defaultValue === 1 + } + + if (type === 'number-input') { + if (typeof defaultValue === 'string' && defaultValue !== '') + target.value = Number.parseFloat(defaultValue) + } + + if (type === 'app-selector' || type === 'model-selector') + target.value = defaultValue + + return target +} + +export const generateFormValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { const newValues = {} as any formSchemas.forEach((formSchema) => { const itemValue = value[formSchema.variable] if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { + const value = formSchema.default newValues[formSchema.variable] = { - ...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }), + value: { + type: 'constant', + value: formSchema.default, + }, + ...(isReasoning ? { auto: 1, value: null } : {}), } + if (!isReasoning) + newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value) } }) return newValues @@ -80,7 +116,9 @@ export const generateFormValue = (value: Record<string, any>, formSchemas: { var export const getPlainValue = (value: Record<string, any>) => { const plainValue = { ...value } Object.keys(plainValue).forEach((key) => { - plainValue[key] = value[key].value + plainValue[key] = { + ...value[key].value, + } }) return plainValue } @@ -94,3 +132,65 @@ export const getStructureValue = (value: Record<string, any>) => { }) return newValue } + +export const getConfiguredValue = (value: Record<string, any>, formSchemas: { variable: string; type: string; default?: any }[]) => { + const newValues = { ...value } + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { + const value = formSchema.default + newValues[formSchema.variable] = { + type: 'constant', + value: formSchema.default, + } + newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value) + } + }) + return newValues +} + +const getVarKindType = (type: FormTypeEnum) => { + if (type === FormTypeEnum.file || type === FormTypeEnum.files) + return VarKindType.variable + if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber) + return VarKindType.constant + if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) + return VarKindType.mixed + } + +export const generateAgentToolValue = (value: Record<string, any>, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { + const newValues = {} as any + if (!isReasoning) { + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + newValues[formSchema.variable] = { + value: { + type: 'constant', + value: itemValue.value, + }, + } + newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value) + }) + } + else { + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + if (itemValue.auto === 1) { + newValues[formSchema.variable] = { + auto: 1, + value: null, + } + } + else { + newValues[formSchema.variable] = { + auto: 0, + value: itemValue.value || { + type: getVarKindType(formSchema.type as FormTypeEnum), + value: null, + }, + } + } + }) + } + return newValues +} diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index f183dd9545..fd48935147 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo, } from 'react' -import { useNodes } from 'reactflow' +import { useStore as useReactflowStore } from 'reactflow' import { RiApps2AddLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { @@ -22,9 +22,8 @@ import { BlockEnum, InputVarType, } from '@/app/components/workflow/types' -import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import { useToastContext } from '@/app/components/base/toast' -import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' +import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' import type { PublishWorkflowParams } from '@/types/workflow' import { fetchAppDetail } from '@/service/apps' import { useStore as useAppStore } from '@/app/components/app/store' @@ -42,9 +41,9 @@ const FeaturesTrigger = () => { const publishedAt = useStore(s => s.publishedAt) const draftUpdatedAt = useStore(s => s.draftUpdatedAt) const toolPublished = useStore(s => s.toolPublished) - const nodes = useNodes<StartNodeType>() - const startNode = nodes.find(node => node.data.type === BlockEnum.Start) - const startVariables = startNode?.data.variables + const startVariables = useReactflowStore( + s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables, + ) const fileSettings = useFeatures(s => s.features.file) const variables = useMemo(() => { const data = startVariables || [] @@ -90,6 +89,7 @@ const FeaturesTrigger = () => { } }, [appID, setAppDetail]) const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) + const updatePublishedWorkflow = useInvalidateAppWorkflow() const onPublish = useCallback(async (params?: PublishWorkflowParams) => { if (await handleCheckBeforePublish()) { const res = await publishWorkflow({ @@ -99,6 +99,7 @@ const FeaturesTrigger = () => { if (res) { notify({ type: 'success', message: t('common.api.actionSuccess') }) + updatePublishedWorkflow(appID!) updateAppDetail() workflowStore.getState().setPublishedAt(res.created_at) resetWorkflowVersionHistory() @@ -107,7 +108,7 @@ const FeaturesTrigger = () => { else { throw new Error('Checklist failed') } - }, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail]) + }, [handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory]) const onPublisherToggle = useCallback((state: boolean) => { if (state) diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 2f2295cb59..d425e6f595 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -7,11 +7,15 @@ import { WorkflowWithInnerContext } from '@/app/components/workflow' import type { WorkflowProps } from '@/app/components/workflow' import WorkflowChildren from './workflow-children' import { + useConfigsMap, + useInspectVarsCrud, useNodesSyncDraft, + useSetWorkflowVarsWithValue, useWorkflowRefreshDraft, useWorkflowRun, useWorkflowStartRun, } from '../hooks' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'> const WorkflowMain = ({ @@ -20,14 +24,28 @@ const WorkflowMain = ({ viewport, }: WorkflowMainProps) => { const featuresStore = useFeaturesStore() + const workflowStore = useWorkflowStore() const handleWorkflowDataUpdate = useCallback((payload: any) => { - if (payload.features && featuresStore) { + const { + features, + conversation_variables, + environment_variables, + } = payload + if (features && featuresStore) { const { setFeatures } = featuresStore.getState() - setFeatures(payload.features) + setFeatures(features) } - }, [featuresStore]) + if (conversation_variables) { + const { setConversationVariables } = workflowStore.getState() + setConversationVariables(conversation_variables) + } + if (environment_variables) { + const { setEnvironmentVariables } = workflowStore.getState() + setEnvironmentVariables(environment_variables) + } + }, [featuresStore, workflowStore]) const { doSyncWorkflowDraft, @@ -46,6 +64,28 @@ const WorkflowMain = ({ handleWorkflowStartRunInChatflow, handleWorkflowStartRunInWorkflow, } = useWorkflowStartRun() + const appId = useStore(s => s.appId) + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId, + ...useConfigsMap(), + }) + const { + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } = useInspectVarsCrud() + const configsMap = useConfigsMap() const hooksStore = useMemo(() => { return { @@ -60,6 +100,22 @@ const WorkflowMain = ({ handleStartWorkflowRun, handleWorkflowStartRunInChatflow, handleWorkflowStartRunInWorkflow, + fetchInspectVars, + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + configsMap, } }, [ syncWorkflowDraftWhenPageClose, @@ -73,6 +129,22 @@ const WorkflowMain = ({ handleStartWorkflowRun, handleWorkflowStartRunInChatflow, handleWorkflowStartRunInWorkflow, + fetchInspectVars, + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + configsMap, ]) return ( diff --git a/web/app/components/workflow-app/hooks/index.ts b/web/app/components/workflow-app/hooks/index.ts index 6373a8591c..9e4f94965b 100644 --- a/web/app/components/workflow-app/hooks/index.ts +++ b/web/app/components/workflow-app/hooks/index.ts @@ -5,3 +5,6 @@ export * from './use-workflow-run' export * from './use-workflow-start-run' export * from './use-is-chat-mode' export * from './use-workflow-refresh-draft' +export * from '../../workflow/hooks/use-fetch-workflow-inspect-vars' +export * from './use-inspect-vars-crud' +export * from './use-configs-map' diff --git a/web/app/components/workflow-app/hooks/use-configs-map.ts b/web/app/components/workflow-app/hooks/use-configs-map.ts new file mode 100644 index 0000000000..0db4f77856 --- /dev/null +++ b/web/app/components/workflow-app/hooks/use-configs-map.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react' +import { useStore } from '@/app/components/workflow/store' + +export const useConfigsMap = () => { + const appId = useStore(s => s.appId) + return useMemo(() => { + return { + conversationVarsUrl: `apps/${appId}/workflows/draft/conversation-variables`, + systemVarsUrl: `apps/${appId}/workflows/draft/system-variables`, + } + }, [appId]) +} diff --git a/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts new file mode 100644 index 0000000000..109ee85f96 --- /dev/null +++ b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts @@ -0,0 +1,16 @@ +import { useStore } from '@/app/components/workflow/store' +import { useInspectVarsCrudCommon } from '../../workflow/hooks/use-inspect-vars-crud-common' +import { useConfigsMap } from './use-configs-map' + +export const useInspectVarsCrud = () => { + const appId = useStore(s => s.appId) + const configsMap = useConfigsMap() + const apis = useInspectVarsCrudCommon({ + flowId: appId, + ...configsMap, + }) + + return { + ...apis, + } +} diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index e1c4c25a4e..6d16dc5c44 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -17,7 +17,6 @@ import { } from '@/service/workflow' import type { FetchWorkflowDraftResponse } from '@/types/workflow' import { useWorkflowConfig } from '@/service/use-workflow' - export const useWorkflowInit = () => { const workflowStore = useWorkflowStore() const { diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 1e484d0760..c303211715 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -19,6 +19,9 @@ import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player import type { VersionHistory } from '@/types/workflow' import { noop } from 'lodash-es' import { useNodesSyncDraft } from './use-nodes-sync-draft' +import { useInvalidAllLastRun } from '@/service/use-workflow' +import { useSetWorkflowVarsWithValue } from '../../workflow/hooks/use-fetch-workflow-inspect-vars' +import { useConfigsMap } from './use-configs-map' export const useWorkflowRun = () => { const store = useStoreApi() @@ -28,6 +31,13 @@ export const useWorkflowRun = () => { const { doSyncWorkflowDraft } = useNodesSyncDraft() const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() const pathname = usePathname() + const appId = useAppStore.getState().appDetail?.id + const invalidAllLastRun = useInvalidAllLastRun(appId as string) + const configsMap = useConfigsMap() + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId as string, + ...configsMap, + }) const { handleWorkflowStarted, @@ -140,11 +150,13 @@ export const useWorkflowRun = () => { clientHeight, } = workflowContainer! + const isInWorkflowDebug = appDetail?.mode === 'workflow' + let url = '' if (appDetail?.mode === 'advanced-chat') url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run` - if (appDetail?.mode === 'workflow') + if (isInWorkflowDebug) url = `/apps/${appDetail.id}/workflows/draft/run` const { @@ -189,6 +201,10 @@ export const useWorkflowRun = () => { if (onWorkflowFinished) onWorkflowFinished(params) + if (isInWorkflowDebug) { + fetchInspectVars() + invalidAllLastRun() + } }, onError: (params) => { handleWorkflowFailed() @@ -292,26 +308,7 @@ export const useWorkflowRun = () => { ...restCallback, }, ) - }, [ - store, - workflowStore, - doSyncWorkflowDraft, - handleWorkflowStarted, - handleWorkflowFinished, - handleWorkflowFailed, - handleWorkflowNodeStarted, - handleWorkflowNodeFinished, - handleWorkflowNodeIterationStarted, - handleWorkflowNodeIterationNext, - handleWorkflowNodeIterationFinished, - handleWorkflowNodeLoopStarted, - handleWorkflowNodeLoopNext, - handleWorkflowNodeLoopFinished, - handleWorkflowNodeRetry, - handleWorkflowTextChunk, - handleWorkflowTextReplace, - handleWorkflowAgentLog, - pathname], + }, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowFailed, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace], ) const handleStopRun = useCallback((taskId: string) => { diff --git a/web/app/components/workflow-app/hooks/use-workflow-template.ts b/web/app/components/workflow-app/hooks/use-workflow-template.ts index 9f47b981dc..2bab08f205 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-template.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-template.ts @@ -22,7 +22,7 @@ export const useWorkflowTemplate = () => { ...nodesInitialData.llm, memory: { window: { enabled: false, size: 10 }, - query_prompt_template: '{{#sys.query#}}', + query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}', }, selected: true, }, diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index 36831aee3c..870d791d4f 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -5,10 +5,11 @@ import { useState, } from 'react' import type { + BlockEnum, OnSelectBlock, ToolWithProvider, } from '../types' -import type { ToolValue } from './types' +import type { ToolDefaultValue, ToolValue } from './types' import { ToolTypeEnum } from './types' import Tools from './tools' import { useToolTabs } from './hooks' @@ -17,8 +18,6 @@ import cn from '@/utils/classnames' import { useGetLanguage } from '@/context/i18n' import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list' import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list' -import ActionButton from '../../base/action-button' -import { RiAddLine } from '@remixicon/react' import { PluginType } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -31,11 +30,12 @@ type AllToolsProps = { buildInTools: ToolWithProvider[] customTools: ToolWithProvider[] workflowTools: ToolWithProvider[] + mcpTools: ToolWithProvider[] onSelect: OnSelectBlock - supportAddCustomTool?: boolean - onAddedCustomTool?: () => void - onShowAddCustomCollectionModal?: () => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const DEFAULT_TAGS: AllToolsProps['tags'] = [] @@ -46,12 +46,14 @@ const AllTools = ({ searchText, tags = DEFAULT_TAGS, onSelect, + canNotSelectMultiple, + onSelectMultiple, buildInTools, workflowTools, customTools, - supportAddCustomTool, - onShowAddCustomCollectionModal, + mcpTools = [], selectedTools, + canChooseMCPTool, }: AllToolsProps) => { const language = useGetLanguage() const tabs = useToolTabs() @@ -64,13 +66,15 @@ const AllTools = ({ const tools = useMemo(() => { let mergedTools: ToolWithProvider[] = [] if (activeTab === ToolTypeEnum.All) - mergedTools = [...buildInTools, ...customTools, ...workflowTools] + mergedTools = [...buildInTools, ...customTools, ...workflowTools, ...mcpTools] if (activeTab === ToolTypeEnum.BuiltIn) mergedTools = buildInTools if (activeTab === ToolTypeEnum.Custom) mergedTools = customTools if (activeTab === ToolTypeEnum.Workflow) mergedTools = workflowTools + if (activeTab === ToolTypeEnum.MCP) + mergedTools = mcpTools if (!hasFilter) return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0) @@ -80,7 +84,7 @@ const AllTools = ({ return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase()) }) }) - }, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter]) + }, [activeTab, buildInTools, customTools, workflowTools, mcpTools, searchText, language, hasFilter]) const { queryPluginsWithDebounced: fetchPlugins, @@ -88,9 +92,8 @@ const AllTools = ({ } = useMarketplacePlugins() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) - useEffect(() => { - if (enable_marketplace) return + if (!enable_marketplace) return if (searchText || tags.length > 0) { fetchPlugins({ query: searchText, @@ -103,10 +106,11 @@ const AllTools = ({ const pluginRef = useRef<ListRef>(null) const wrapElemRef = useRef<HTMLDivElement>(null) + const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab) return ( - <div className={cn(className)}> - <div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'> + <div className={cn('min-w-[400px] max-w-[500px]', className)}> + <div className='flex items-center justify-between border-b border-divider-subtle px-3'> <div className='flex h-8 items-center space-x-1'> { tabs.map(tab => ( @@ -124,17 +128,8 @@ const AllTools = ({ )) } </div> - <ViewTypeSelect viewType={activeView} onChange={setActiveView} /> - {supportAddCustomTool && ( - <div className='flex items-center'> - <div className='mr-1.5 h-3.5 w-px bg-divider-regular'></div> - <ActionButton - className='bg-components-button-primary-bg text-components-button-primary-text hover:bg-components-button-primary-bg hover:text-components-button-primary-text' - onClick={onShowAddCustomCollectionModal} - > - <RiAddLine className='h-4 w-4' /> - </ActionButton> - </div> + {isSupportGroupView && ( + <ViewTypeSelect viewType={activeView} onChange={setActiveView} /> )} </div> <div @@ -144,12 +139,15 @@ const AllTools = ({ > <Tools className={toolContentClassName} - showWorkflowEmpty={activeTab === ToolTypeEnum.Workflow} tools={tools} onSelect={onSelect} - viewType={activeView} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} + toolType={activeTab} + viewType={isSupportGroupView ? activeView : ViewType.flat} hasSearchText={!!searchText} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> {/* Plugins from marketplace */} {enable_marketplace && <PluginList diff --git a/web/app/components/workflow/block-selector/hooks.ts b/web/app/components/workflow/block-selector/hooks.ts index a8b1759506..d00815584d 100644 --- a/web/app/components/workflow/block-selector/hooks.ts +++ b/web/app/components/workflow/block-selector/hooks.ts @@ -31,10 +31,9 @@ export const useTabs = () => { ] } -export const useToolTabs = () => { +export const useToolTabs = (isHideMCPTools?: boolean) => { const { t } = useTranslation() - - return [ + const tabs = [ { key: ToolTypeEnum.All, name: t('workflow.tabs.allTool'), @@ -52,4 +51,12 @@ export const useToolTabs = () => { name: t('workflow.tabs.workflowTool'), }, ] + if(!isHideMCPTools) { + tabs.push({ + key: ToolTypeEnum.MCP, + name: 'MCP', + }) + } + + return tabs } diff --git a/web/app/components/workflow/block-selector/index-bar.tsx b/web/app/components/workflow/block-selector/index-bar.tsx index 4d8bedffbe..097a16eb94 100644 --- a/web/app/components/workflow/block-selector/index-bar.tsx +++ b/web/app/components/workflow/block-selector/index-bar.tsx @@ -83,8 +83,8 @@ const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => { element.scrollIntoView({ behavior: 'smooth' }) } return ( - <div className={classNames('index-bar absolute right-0 top-36 flex flex-col items-center w-6 justify-center text-xs font-medium text-text-quaternary', className)}> - <div className='absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]'></div> + <div className={classNames('index-bar sticky top-[20px] flex h-full w-6 flex-col items-center justify-center text-xs font-medium text-text-quaternary', className)}> + <div className={classNames('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div> {letters.map(letter => ( <div className="cursor-pointer hover:text-text-secondary" key={letter} onClick={() => handleIndexClick(letter)}> {letter} diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 9e55a24d9e..0673ca0c0d 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -129,33 +129,35 @@ const NodeSelector: FC<NodeSelectorProps> = ({ </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[1000]'> <div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}> - <div className='px-2 pt-2' onClick={e => e.stopPropagation()}> - {activeTab === TabsEnum.Blocks && ( - <Input - showLeftIcon - showClearIcon - autoFocus - value={searchText} - placeholder={searchPlaceholder} - onChange={e => setSearchText(e.target.value)} - onClear={() => setSearchText('')} - /> - )} - {activeTab === TabsEnum.Tools && ( - <SearchBox - search={searchText} - onSearchChange={setSearchText} - tags={tags} - onTagsChange={setTags} - size='small' - placeholder={t('plugin.searchTools')!} - /> - )} - - </div> <Tabs activeTab={activeTab} onActiveTabChange={handleActiveTabChange} + filterElem={ + <div className='relative m-2' onClick={e => e.stopPropagation()}> + {activeTab === TabsEnum.Blocks && ( + <Input + showLeftIcon + showClearIcon + autoFocus + value={searchText} + placeholder={searchPlaceholder} + onChange={e => setSearchText(e.target.value)} + onClear={() => setSearchText('')} + /> + )} + {activeTab === TabsEnum.Tools && ( + <SearchBox + search={searchText} + onSearchChange={setSearchText} + tags={tags} + onTagsChange={setTags} + size='small' + placeholder={t('plugin.searchTools')!} + inputClassName='grow' + /> + )} + </div> + } onSelect={handleSelect} searchText={searchText} tags={tags} diff --git a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx index 9c3c69d601..4e64929582 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx @@ -12,9 +12,9 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import cn from '@/utils/classnames' -import { MARKETPLACE_URL_PREFIX } from '@/config' import { useDownloadPlugin } from '@/service/use-plugins' import { downloadFile } from '@/utils/format' +import { getMarketplaceUrl } from '@/utils/var' type Props = { open: boolean @@ -80,7 +80,7 @@ const OperationDropdown: FC<Props> = ({ <PortalToFollowElemContent className='z-[9999]'> <div className='w-[112px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'> <div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div> - <a href={`${MARKETPLACE_URL_PREFIX}/plugins/${author}/${name}${theme ? `?theme=${theme}` : ''}`} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> + <a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a> </div> </PortalToFollowElemContent> </PortalToFollowElem> diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index 27cd2ae3ea..dce877ab91 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -6,9 +6,9 @@ import Item from './item' import type { Plugin } from '@/app/components/plugins/types.ts' import cn from '@/utils/classnames' import Link from 'next/link' -import { marketplaceUrlPrefix } from '@/config' import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react' import { noop } from 'lodash-es' +import { getMarketplaceUrl } from '@/utils/var' export type ListProps = { wrapElemRef: React.RefObject<HTMLElement> @@ -32,7 +32,7 @@ const List = forwardRef<ListRef, ListProps>(({ const { t } = useTranslation() const hasFilter = !searchText const hasRes = list.length > 0 - const urlWithSearchText = `${marketplaceUrlPrefix}/?q=${searchText}&tags=${tags.join(',')}` + const urlWithSearchText = getMarketplaceUrl('', { q: searchText, tags: tags.join(',') }) const nextToStickyELemRef = useRef<HTMLDivElement>(null) const { handleScroll, scrollPosition } = useStickyScroll({ @@ -71,7 +71,7 @@ const List = forwardRef<ListRef, ListProps>(({ return ( <Link className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg' - href={`${marketplaceUrlPrefix}/`} + href={getMarketplaceUrl('')} target='_blank' > <span>{t('plugin.findMoreInMarketplace')}</span> @@ -80,7 +80,7 @@ const List = forwardRef<ListRef, ListProps>(({ ) } - const maxWidthClassName = toolContentClassName || 'max-w-[300px]' + const maxWidthClassName = toolContentClassName || 'max-w-[100%]' return ( <> @@ -109,18 +109,20 @@ const List = forwardRef<ListRef, ListProps>(({ onAction={noop} /> ))} - <div className='mb-3 mt-2 flex items-center justify-center space-x-2'> - <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div> - <Link - href={urlWithSearchText} - target='_blank' - className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only' - > - <RiSearchLine className='mr-0.5 h-3 w-3' /> - <span>{t('plugin.searchInMarketplace')}</span> - </Link> - <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div> - </div> + {list.length > 0 && ( + <div className='mb-3 mt-2 flex items-center justify-center space-x-2'> + <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div> + <Link + href={urlWithSearchText} + target='_blank' + className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only' + > + <RiSearchLine className='mr-0.5 h-3 w-3' /> + <span>{t('plugin.searchInMarketplace')}</span> + </Link> + <div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div> + </div> + )} </div> </> ) diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 67aaaba1a5..3f3fed2ca9 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { memo } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' import type { BlockEnum } from '../types' import { useTabs } from './hooks' import type { ToolDefaultValue } from './types' @@ -16,6 +16,7 @@ export type TabsProps = { tags: string[] onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void availableBlocksTypes?: BlockEnum[] + filterElem: React.ReactNode noBlocks?: boolean } const Tabs: FC<TabsProps> = ({ @@ -25,26 +26,28 @@ const Tabs: FC<TabsProps> = ({ searchText, onSelect, availableBlocksTypes, + filterElem, noBlocks, }) => { const tabs = useTabs() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() return ( <div onClick={e => e.stopPropagation()}> { !noBlocks && ( - <div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'> + <div className='relative flex bg-background-section-burn pl-1 pt-1'> { tabs.map(tab => ( <div key={tab.key} className={cn( - 'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1', + 'system-sm-medium relative mr-0.5 flex h-8 cursor-pointer items-center rounded-t-lg px-3 ', activeTab === tab.key - ? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600' + ? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent' : 'text-text-tertiary', )} onClick={() => onActiveTabChange(tab.key)} @@ -56,25 +59,30 @@ const Tabs: FC<TabsProps> = ({ </div> ) } + {filterElem} { activeTab === TabsEnum.Blocks && !noBlocks && ( - <Blocks - searchText={searchText} - onSelect={onSelect} - availableBlocksTypes={availableBlocksTypes} - /> + <div className='border-t border-divider-subtle'> + <Blocks + searchText={searchText} + onSelect={onSelect} + availableBlocksTypes={availableBlocksTypes} + /> + </div> ) } { activeTab === TabsEnum.Tools && ( <AllTools - className='w-[315px]' searchText={searchText} onSelect={onSelect} tags={tags} + canNotSelectMultiple buildInTools={buildInTools || []} customTools={customTools || []} workflowTools={workflowTools || []} + mcpTools={mcpTools || []} + canChooseMCPTool /> ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index dbb49fde75..d97a4f3a1b 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -23,7 +23,7 @@ import { } from '@/service/tools' import type { CustomCollectionBackend } from '@/app/components/tools/types' import Toast from '@/app/components/base/toast' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' import cn from '@/utils/classnames' type Props = { @@ -35,9 +35,11 @@ type Props = { isShow: boolean onShowChange: (isShow: boolean) => void onSelect: (tool: ToolDefaultValue) => void + onSelectMultiple: (tools: ToolDefaultValue[]) => void supportAddCustomTool?: boolean scope?: string selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolPicker: FC<Props> = ({ @@ -48,10 +50,12 @@ const ToolPicker: FC<Props> = ({ isShow, onShowChange, onSelect, + onSelectMultiple, supportAddCustomTool, scope = 'all', selectedTools, panelClassName, + canChooseMCPTool, }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') @@ -61,6 +65,7 @@ const ToolPicker: FC<Props> = ({ const { data: customTools } = useAllCustomTools() const invalidateCustomTools = useInvalidateAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { if (scope === 'plugins') { @@ -102,6 +107,10 @@ const ToolPicker: FC<Props> = ({ onSelect(tool!) } + const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => { + onSelectMultiple(tools) + } + const [isShowEditCollectionToolModal, { setFalse: hideEditCustomCollectionModal, setTrue: showEditCustomCollectionModal, @@ -142,7 +151,7 @@ const ToolPicker: FC<Props> = ({ </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[1000]'> - <div className={cn('relative min-h-20 w-[356px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}> + <div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}> <div className='p-2 pb-1'> <SearchBox search={searchText} @@ -151,21 +160,26 @@ const ToolPicker: FC<Props> = ({ onTagsChange={setTags} size='small' placeholder={t('plugin.searchTools')!} + supportAddCustomTool={supportAddCustomTool} + onAddedCustomTool={handleAddedCustomTool} + onShowAddCustomCollectionModal={showEditCustomCollectionModal} + inputClassName='grow' + /> </div> <AllTools className='mt-1' - toolContentClassName='max-w-[360px]' + toolContentClassName='max-w-[100%]' tags={tags} searchText={searchText} onSelect={handleSelect} + onSelectMultiple={handleSelectMultiple} buildInTools={builtinToolList || []} customTools={customToolList || []} workflowTools={workflowToolList || []} - supportAddCustomTool={supportAddCustomTool} - onAddedCustomTool={handleAddedCustomTool} - onShowAddCustomCollectionModal={showEditCustomCollectionModal} + mcpTools={mcpTools || []} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> </div> </PortalToFollowElemContent> diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index dc9b9b9114..e5e33614b0 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -10,13 +10,12 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import cn from '@/utils/classnames' import { useTranslation } from 'react-i18next' -import { RiCheckLine } from '@remixicon/react' -import Badge from '@/app/components/base/badge' type Props = { provider: ToolWithProvider payload: Tool disabled?: boolean + isAdded?: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void } @@ -25,6 +24,7 @@ const ToolItem: FC<Props> = ({ payload, onSelect, disabled, + isAdded, }) => { const { t } = useTranslation() @@ -71,18 +71,16 @@ const ToolItem: FC<Props> = ({ output_schema: payload.output_schema, paramSchemas: payload.parameters, params, + meta: provider.meta, }) }} > - <div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary', disabled && 'opacity-30')}>{payload.label[language]}</div> - {disabled && <Badge - className='flex h-5 items-center space-x-0.5 text-text-tertiary' - uppercase - > - <RiCheckLine className='h-3 w-3 ' /> - <div>{t('tools.addToolModal.added')}</div> - </Badge> - } + <div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}> + <span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span> + </div> + {isAdded && ( + <div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div> + )} </div> </Tooltip > ) diff --git a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx index ef671ca1f8..ca462c082e 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx @@ -11,21 +11,29 @@ import { useMemo } from 'react' type Props = { payload: ToolWithProvider[] isShowLetterIndex: boolean + indexBar: React.ReactNode hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void letters: string[] toolRefs: any selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolViewFlatView: FC<Props> = ({ letters, payload, isShowLetterIndex, + indexBar, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, toolRefs, selectedTools, + canChooseMCPTool, }) => { const firstLetterToolIds = useMemo(() => { const res: Record<string, string> = {} @@ -37,26 +45,31 @@ const ToolViewFlatView: FC<Props> = ({ return res }, [payload, letters]) return ( - <div> - {payload.map(tool => ( - <div - key={tool.id} - ref={(el) => { - const letter = firstLetterToolIds[tool.id] - if (letter) - toolRefs.current[letter] = el - }} - > - <Tool - payload={tool} - viewType={ViewType.flat} - isShowLetterIndex={isShowLetterIndex} - hasSearchText={hasSearchText} - onSelect={onSelect} - selectedTools={selectedTools} - /> - </div> - ))} + <div className='flex w-full'> + <div className='mr-1 grow'> + {payload.map(tool => ( + <div + key={tool.id} + ref={(el) => { + const letter = firstLetterToolIds[tool.id] + if (letter) + toolRefs.current[letter] = el + }} + > + <Tool + payload={tool} + viewType={ViewType.flat} + hasSearchText={hasSearchText} + onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} + selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} + /> + </div> + ))} + </div> + {isShowLetterIndex && indexBar} </div> ) } diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx index d6c567f8e2..b3f7aab4df 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx @@ -12,7 +12,10 @@ type Props = { toolList: ToolWithProvider[] hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Item: FC<Props> = ({ @@ -20,7 +23,10 @@ const Item: FC<Props> = ({ toolList, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { return ( <div> @@ -36,7 +42,10 @@ const Item: FC<Props> = ({ isShowLetterIndex={false} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> ))} </div> diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx index f3f98279c8..d85d1ea682 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx @@ -12,14 +12,20 @@ type Props = { payload: Record<string, ToolWithProvider[]> hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolListTreeView: FC<Props> = ({ payload, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { const { t } = useTranslation() const getI18nGroupName = useCallback((name: string) => { @@ -46,7 +52,10 @@ const ToolListTreeView: FC<Props> = ({ toolList={payload[groupName]} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> ))} </div> diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index f135b5bf4e..83ae062737 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import cn from '@/utils/classnames' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import { useGetLanguage } from '@/context/i18n' @@ -10,39 +10,111 @@ import type { ToolWithProvider } from '../../types' import { BlockEnum } from '../../types' import type { ToolDefaultValue, ToolValue } from '../types' import { ViewType } from '../view-type-select' -import ActonItem from './action-item' +import ActionItem from './action-item' import BlockIcon from '../../block-icon' import { useTranslation } from 'react-i18next' +import { useHover } from 'ahooks' +import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' +import { Mcp } from '@/app/components/base/icons/src/vender/other' type Props = { className?: string payload: ToolWithProvider viewType: ViewType - isShowLetterIndex: boolean hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Tool: FC<Props> = ({ className, payload, viewType, - isShowLetterIndex, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { const { t } = useTranslation() const language = useGetLanguage() const isFlatView = viewType === ViewType.flat + const notShowProvider = payload.type === CollectionType.workflow const actions = payload.tools - const hasAction = true // Now always support actions + const hasAction = !notShowProvider const [isFold, setFold] = React.useState<boolean>(true) - const getIsDisabled = (tool: ToolType) => { + const ref = useRef(null) + const isHovering = useHover(ref) + const isMCPTool = payload.type === CollectionType.mcp + const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool + const getIsDisabled = useCallback((tool: ToolType) => { if (!selectedTools || !selectedTools.length) return false - return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name) - } + return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name) + }, [payload.id, payload.name, selectedTools]) + + const totalToolsNum = actions.length + const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length + const isAllSelected = selectedToolsNum === totalToolsNum + + const notShowProviderSelectInfo = useMemo(() => { + if (isAllSelected) { + return ( + <span className='system-xs-regular text-text-tertiary'> + {t('tools.addToolModal.added')} + </span> + ) + } + }, [isAllSelected, t]) + const selectedInfo = useMemo(() => { + if (isHovering && !isAllSelected) { + return ( + <span className='system-xs-regular text-components-button-secondary-accent-text' + onClick={(e) => { + onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => { + const params: Record<string, string> = {} + if (tool.parameters) { + tool.parameters.forEach((item) => { + params[item.name] = '' + }) + } + return { + provider_id: payload.id, + provider_type: payload.type, + provider_name: payload.name, + tool_name: tool.name, + tool_label: tool.label[language], + tool_description: tool.description[language], + title: tool.label[language], + is_team_authorization: payload.is_team_authorization, + output_schema: tool.output_schema, + paramSchemas: tool.parameters, + params, + } + })) + }} + > + {t('workflow.tabs.addAll')} + </span> + ) + } + + if (selectedToolsNum === 0) + return <></> + + return ( + <span className='system-xs-regular text-text-tertiary'> + {isAllSelected + ? t('workflow.tabs.allAdded') + : `${selectedToolsNum} / ${totalToolsNum}` + } + </span> + ) + }, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum]) + useEffect(() => { if (hasSearchText && isFold) { setFold(false) @@ -71,59 +143,73 @@ const Tool: FC<Props> = ({ return ( <div key={payload.id} - className={cn('mb-1 last-of-type:mb-0', isShowLetterIndex && 'mr-6')} + className={cn('mb-1 last-of-type:mb-0')} + ref={ref} > <div className={cn(className)}> <div - className='flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover' + className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover' onClick={() => { - if (hasAction) + if (hasAction) { setFold(!isFold) + return + } - // Now always support actions - // if (payload.parameters) { - // payload.parameters.forEach((item) => { - // params[item.name] = '' - // }) - // } - // onSelect(BlockEnum.Tool, { - // provider_id: payload.id, - // provider_type: payload.type, - // provider_name: payload.name, - // tool_name: payload.name, - // tool_label: payload.label[language], - // title: payload.label[language], - // params: {}, - // }) + const tool = actions[0] + const params: Record<string, string> = {} + if (tool.parameters) { + tool.parameters.forEach((item) => { + params[item.name] = '' + }) + } + onSelect(BlockEnum.Tool, { + provider_id: payload.id, + provider_type: payload.type, + provider_name: payload.name, + tool_name: tool.name, + tool_label: tool.label[language], + tool_description: tool.description[language], + title: tool.label[language], + is_team_authorization: payload.is_team_authorization, + output_schema: tool.output_schema, + paramSchemas: tool.parameters, + params, + }) }} > - <div className='flex h-8 grow items-center'> + <div className={cn('flex h-8 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}> <BlockIcon className='shrink-0' type={BlockEnum.Tool} toolIcon={payload.icon} /> - <div className='ml-2 w-0 flex-1 grow truncate text-sm text-text-primary'>{payload.label[language]}</div> + <div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'> + <span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span> + {isFlatView && groupName && ( + <span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span> + )} + {isMCPTool && <Mcp className='ml-2 size-3.5 shrink-0 text-text-quaternary' />} + </div> </div> - <div className='flex items-center'> - {isFlatView && ( - <div className='system-xs-regular text-text-tertiary'>{groupName}</div> - )} + <div className='ml-2 flex items-center'> + {!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)} + {isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />} {hasAction && ( - <FoldIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary', isFold && 'text-text-tertiary')} /> + <FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} /> )} </div> </div> - {hasAction && !isFold && ( + {!notShowProvider && hasAction && !isFold && ( actions.map(action => ( - <ActonItem + <ActionItem key={action.name} provider={payload} payload={action} onSelect={onSelect} - disabled={getIsDisabled(action)} + disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip} + isAdded={getIsDisabled(action)} /> )) )} diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx index 2562501524..da47432b04 100644 --- a/web/app/components/workflow/block-selector/tools.tsx +++ b/web/app/components/workflow/block-selector/tools.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import type { BlockEnum, ToolWithProvider } from '../types' import IndexBar, { groupItems } from './index-bar' import type { ToolDefaultValue, ToolValue } from './types' +import type { ToolTypeEnum } from './types' import { ViewType } from './view-type-select' import Empty from '@/app/components/tools/add-tool-modal/empty' import { useGetLanguage } from '@/context/i18n' @@ -15,25 +16,34 @@ import ToolListFlatView from './tool/tool-list-flat-view/list' import classNames from '@/utils/classnames' type ToolsProps = { - showWorkflowEmpty: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void tools: ToolWithProvider[] viewType: ViewType hasSearchText: boolean + toolType?: ToolTypeEnum + isAgent?: boolean className?: string indexBarClassName?: string selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Blocks = ({ - showWorkflowEmpty, onSelect, + canNotSelectMultiple, + onSelectMultiple, tools, viewType, hasSearchText, + toolType, + isAgent, className, indexBarClassName, selectedTools, + canChooseMCPTool, }: ToolsProps) => { + // const tools: any = [] const { t } = useTranslation() const language = useGetLanguage() const isFlatView = viewType === ViewType.flat @@ -87,15 +97,15 @@ const Blocks = ({ const toolRefs = useRef({}) return ( - <div className={classNames('p-1 max-w-[320px]', className)}> + <div className={classNames('max-w-[100%] p-1', className)}> { - !tools.length && !showWorkflowEmpty && ( - <div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div> + !tools.length && hasSearchText && ( + <div className='mt-2 flex h-[22px] items-center px-3 text-xs font-medium text-text-secondary'>{t('workflow.tabs.noResult')}</div> ) } - {!tools.length && showWorkflowEmpty && ( + {!tools.length && !hasSearchText && ( <div className='py-10'> - <Empty /> + <Empty type={toolType!} isAgent={isAgent}/> </div> )} {!!tools.length && ( @@ -107,19 +117,24 @@ const Blocks = ({ isShowLetterIndex={isShowLetterIndex} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} + indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />} /> ) : ( <ToolListTreeView payload={treeViewToolsData} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> ) )} - - {isShowLetterIndex && <IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />} </div> ) } diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 0abf7b9031..c96a60f674 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -1,3 +1,5 @@ +import type { PluginMeta } from '../../plugins/types' + export enum TabsEnum { Blocks = 'blocks', Tools = 'tools', @@ -8,6 +10,7 @@ export enum ToolTypeEnum { BuiltIn = 'built-in', Custom = 'custom', Workflow = 'workflow', + MCP = 'mcp', } export enum BlockClassificationEnum { @@ -30,13 +33,15 @@ export type ToolDefaultValue = { params: Record<string, any> paramSchemas: Record<string, any>[] output_schema: Record<string, any> + meta?: PluginMeta } export type ToolValue = { provider_name: string + provider_show_name?: string tool_name: string tool_label: string - tool_description: string + tool_description?: string settings?: Record<string, any> parameters?: Record<string, any> enabled?: boolean diff --git a/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts b/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts new file mode 100644 index 0000000000..98986cf3b6 --- /dev/null +++ b/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react' + +const useCheckVerticalScrollbar = (ref: React.RefObject<HTMLElement>) => { + const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false) + + useEffect(() => { + const elem = ref.current + if (!elem) return + + const checkScrollbar = () => { + setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight) + } + + checkScrollbar() + + const resizeObserver = new ResizeObserver(checkScrollbar) + resizeObserver.observe(elem) + + const mutationObserver = new MutationObserver(checkScrollbar) + mutationObserver.observe(elem, { childList: true, subtree: true, characterData: true }) + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + } + }, [ref]) + + return hasVerticalScrollbar +} + +export default useCheckVerticalScrollbar diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index cdfd963cfa..0ef4dc9dea 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -31,6 +31,7 @@ type NodesExtraData = { getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[] getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[] checkValid: any + defaultRunInputData?: Record<string, any> } export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = { [BlockEnum.Start]: { @@ -68,6 +69,7 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = { getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes, getAvailableNextNodes: LLMDefault.getAvailableNextNodes, checkValid: LLMDefault.checkValid, + defaultRunInputData: LLMDefault.defaultRunInputData, }, [BlockEnum.KnowledgeRetrieval]: { author: 'Dify', @@ -408,7 +410,6 @@ export const NODE_WIDTH = 240 export const X_OFFSET = 60 export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET export const Y_OFFSET = 39 -export const MAX_TREE_DEPTH = 50 export const START_INITIAL_POSITION = { x: 80, y: 282 } export const AUTO_LAYOUT_OFFSET = { x: -42, @@ -479,6 +480,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [ variable: 'text', type: VarType.string, }, + { + variable: 'usage', + type: VarType.object, + }, ] export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [ @@ -500,6 +505,10 @@ export const QUESTION_CLASSIFIER_OUTPUT_STRUCT = [ variable: 'class_name', type: VarType.string, }, + { + variable: 'usage', + type: VarType.object, + }, ] export const HTTP_REQUEST_OUTPUT_STRUCT: Var[] = [ @@ -545,6 +554,10 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [ variable: '__reason', type: VarType.string, }, + { + variable: '__usage', + type: VarType.object, + }, ] export const FILE_STRUCT: Var[] = [ diff --git a/web/app/components/workflow/header/header-in-normal.tsx b/web/app/components/workflow/header/header-in-normal.tsx index ec016b1b65..5768e6bc06 100644 --- a/web/app/components/workflow/header/header-in-normal.tsx +++ b/web/app/components/workflow/header/header-in-normal.tsx @@ -33,6 +33,8 @@ const HeaderInNormal = ({ const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) const setShowEnvPanel = useStore(s => s.setShowEnvPanel) const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel) + const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) + const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel) const nodes = useNodes<StartNodeType>() const selectedNode = nodes.find(node => node.data.selected) const { handleBackupDraft } = useWorkflowRun() @@ -46,8 +48,10 @@ const HeaderInNormal = ({ setShowWorkflowVersionHistoryPanel(true) setShowEnvPanel(false) setShowDebugAndPreviewPanel(false) + setShowVariableInspectPanel(false) + setShowChatVariablePanel(false) }, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode, - setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel]) + setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel]) return ( <> diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index 4d1954587d..afa4e62099 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -17,6 +17,8 @@ import { import Toast from '../../base/toast' import RestoringTitle from './restoring-title' import Button from '@/app/components/base/button' +import { useStore as useAppStore } from '@/app/components/app/store' +import { useInvalidAllLastRun } from '@/service/use-workflow' export type HeaderInRestoringProps = { onRestoreSettled?: () => void @@ -26,6 +28,12 @@ const HeaderInRestoring = ({ }: HeaderInRestoringProps) => { const { t } = useTranslation() const workflowStore = useWorkflowStore() + const appDetail = useAppStore.getState().appDetail + + const invalidAllLastRun = useInvalidAllLastRun(appDetail!.id) + const { + deleteAllInspectVars, + } = workflowStore.getState() const currentVersion = useStore(s => s.currentVersion) const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) @@ -61,7 +69,9 @@ const HeaderInRestoring = ({ onRestoreSettled?.() }, }) - }, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, onRestoreSettled, t]) + deleteAllInspectVars() + invalidAllLastRun() + }, [setShowWorkflowVersionHistoryPanel, workflowStore, handleSyncWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, onRestoreSettled]) return ( <> diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index e5391afb09..7713753478 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -1,3 +1,4 @@ +import { usePathname } from 'next/navigation' import { useWorkflowMode, } from '../hooks' @@ -6,7 +7,7 @@ import HeaderInNormal from './header-in-normal' import HeaderInHistory from './header-in-view-history' import type { HeaderInRestoringProps } from './header-in-restoring' import HeaderInRestoring from './header-in-restoring' - +import { useStore } from '../store' export type HeaderProps = { normal?: HeaderInNormalProps restoring?: HeaderInRestoringProps @@ -15,16 +16,20 @@ const Header = ({ normal: normalProps, restoring: restoringProps, }: HeaderProps) => { + const pathname = usePathname() + const inWorkflowCanvas = pathname.endsWith('/workflow') const { normal, restoring, viewHistory, } = useWorkflowMode() + const maximizeCanvas = useStore(s => s.maximizeCanvas) return ( <div className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3' > + {inWorkflowCanvas && maximizeCanvas && <div className='h-14 w-[52px]' />} { normal && ( <HeaderInNormal diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index 19ed87d990..225e2805a5 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -19,6 +19,8 @@ import cn from '@/utils/classnames' import { StopCircle, } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' const RunMode = memo(() => { const { t } = useTranslation() @@ -27,6 +29,16 @@ const RunMode = memo(() => { const workflowRunningData = useStore(s => s.workflowRunningData) const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running + const handleStop = () => { + handleStopRun(workflowRunningData?.task_id || '') + } + + const { eventEmitter } = useEventEmitterContextContext() + eventEmitter?.useSubscription((v: any) => { + if (v.type === EVENT_WORKFLOW_STOP) + handleStop() + }) + return ( <> <div @@ -59,7 +71,7 @@ const RunMode = memo(() => { isRunning && ( <div className='ml-0.5 flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-black/5' - onClick={() => handleStopRun(workflowRunningData?.task_id || '')} + onClick={handleStop} > <StopCircle className='h-4 w-4 text-components-button-ghost-text' /> </div> diff --git a/web/app/components/workflow/header/running-title.tsx b/web/app/components/workflow/header/running-title.tsx index 0460a96c91..95e763ead7 100644 --- a/web/app/components/workflow/header/running-title.tsx +++ b/web/app/components/workflow/header/running-title.tsx @@ -2,6 +2,7 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useIsChatMode } from '../hooks' import { useStore } from '../store' +import { formatWorkflowRunIdentifier } from '../utils' import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time' const RunningTitle = () => { @@ -12,7 +13,7 @@ const RunningTitle = () => { return ( <div className='flex h-[18px] items-center text-xs text-gray-500'> <ClockPlay className='mr-1 h-3 w-3 text-gray-500' /> - <span>{isChatMode ? `Test Chat#${historyWorkflowData?.sequence_number}` : `Test Run#${historyWorkflowData?.sequence_number}`}</span> + <span>{isChatMode ? `Test Chat${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}` : `Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`}</span> <span className='mx-1'>·</span> <span className='ml-1 flex h-[18px] items-center rounded-[5px] border border-indigo-300 bg-white/[0.48] px-1 text-[10px] font-semibold uppercase text-indigo-600'> {t('workflow.common.viewOnly')} diff --git a/web/app/components/workflow/header/view-history.tsx b/web/app/components/workflow/header/view-history.tsx index 21b4462867..08f952cd1a 100644 --- a/web/app/components/workflow/header/view-history.tsx +++ b/web/app/components/workflow/header/view-history.tsx @@ -18,6 +18,7 @@ import { useWorkflowRun, } from '../hooks' import { ControlMode, WorkflowRunningStatus } from '../types' +import { formatWorkflowRunIdentifier } from '../utils' import cn from '@/utils/classnames' import { PortalToFollowElem, @@ -199,7 +200,7 @@ const ViewHistory = ({ item.id === historyWorkflowData?.id && 'text-text-accent', )} > - {`Test ${isChatMode ? 'Chat' : 'Run'} #${item.sequence_number}`} + {`Test ${isChatMode ? 'Chat' : 'Run'}${formatWorkflowRunIdentifier(item.finished_at)}`} </div> <div className='flex items-center text-xs leading-[18px] text-text-tertiary'> {item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)} diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index 9f5e1a6650..4e8f74c774 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -7,6 +7,12 @@ import { } from 'zustand' import { createStore } from 'zustand/vanilla' import { HooksStoreContext } from './provider' +import type { IOtherOptions } from '@/service/base' +import type { VarInInspect } from '@/types/workflow' +import type { + Node, + ValueSelector, +} from '@/app/components/workflow/types' type CommonHooksFnMap = { doSyncWorkflowDraft: ( @@ -22,11 +28,30 @@ type CommonHooksFnMap = { handleBackupDraft: () => void handleLoadBackupDraft: () => void handleRestoreFromPublishedWorkflow: (...args: any[]) => void - handleRun: (...args: any[]) => void + handleRun: (params: any, callback?: IOtherOptions,) => void handleStopRun: (...args: any[]) => void handleStartWorkflowRun: () => void handleWorkflowStartRunInWorkflow: () => void handleWorkflowStartRunInChatflow: () => void + fetchInspectVars: () => Promise<void> + hasNodeInspectVars: (nodeId: string) => boolean + hasSetInspectVar: (nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => boolean + fetchInspectVarValue: (selector: ValueSelector) => Promise<void> + editInspectVarValue: (nodeId: string, varId: string, value: any) => Promise<void> + renameInspectVarName: (nodeId: string, oldName: string, newName: string) => Promise<void> + appendNodeInspectVars: (nodeId: string, payload: VarInInspect[], allNodes: Node[]) => void + deleteInspectVar: (nodeId: string, varId: string) => Promise<void> + deleteNodeInspectorVars: (nodeId: string) => Promise<void> + deleteAllInspectorVars: () => Promise<void> + isInspectVarEdited: (nodeId: string, name: string) => boolean + resetToLastRunVar: (nodeId: string, varId: string) => Promise<void> + invalidateSysVarValues: () => void + resetConversationVar: (varId: string) => Promise<void> + invalidateConversationVarValues: () => void + configsMap?: { + conversationVarsUrl: string + systemVarsUrl: string + } } export type Shape = { @@ -45,6 +70,21 @@ export const createHooksStore = ({ handleStartWorkflowRun = noop, handleWorkflowStartRunInWorkflow = noop, handleWorkflowStartRunInChatflow = noop, + fetchInspectVars = async () => noop(), + hasNodeInspectVars = () => false, + hasSetInspectVar = () => false, + fetchInspectVarValue = async () => noop(), + editInspectVarValue = async () => noop(), + renameInspectVarName = async () => noop(), + appendNodeInspectVars = () => noop(), + deleteInspectVar = async () => noop(), + deleteNodeInspectorVars = async () => noop(), + deleteAllInspectorVars = async () => noop(), + isInspectVarEdited = () => false, + resetToLastRunVar = async () => noop(), + invalidateSysVarValues = noop, + resetConversationVar = async () => noop(), + invalidateConversationVarValues = noop, }: Partial<Shape>) => { return createStore<Shape>(set => ({ refreshAll: props => set(state => ({ ...state, ...props })), @@ -59,6 +99,21 @@ export const createHooksStore = ({ handleStartWorkflowRun, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow, + fetchInspectVars, + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, })) } diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index fda0f50aa6..c2eebb0533 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -17,3 +17,5 @@ export * from './use-workflow-interactions' export * from './use-workflow-mode' export * from './use-format-time-from-now' export * from './use-workflow-refresh-draft' +export * from './use-inspect-vars-crud' +export * from './use-set-workflow-vars-with-value' diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index c1b0189b8b..47ebaa6710 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -9,6 +9,7 @@ import type { CommonNodeType, Edge, Node, + ValueSelector, } from '../types' import { BlockEnum } from '../types' import { useStore } from '../store' @@ -18,7 +19,6 @@ import { } from '../utils' import { CUSTOM_NODE, - MAX_TREE_DEPTH, } from '../constants' import type { ToolNodeType } from '../nodes/tool/types' import { useIsChatMode } from './use-workflow' @@ -33,6 +33,9 @@ import { useDatasetsDetailStore } from '../datasets-detail-store/store' import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types' import type { DataSet } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' +import { MAX_TREE_DEPTH } from '@/config' +import useNodesAvailableVarList from './use-nodes-available-var-list' +import { getNodeUsedVars, isConversationVar, isENV, isSystemVar } from '../nodes/_base/components/variable/utils' export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { t } = useTranslation() @@ -45,6 +48,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const { data: strategyProviders } = useStrategyProviders() const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail) + const map = useNodesAvailableVarList(nodes) + const getCheckData = useCallback((data: CommonNodeType<{}>) => { let checkData = data if (data.type === BlockEnum.KnowledgeRetrieval) { @@ -70,6 +75,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const node = nodes[i] let toolIcon let moreDataForCheckValid + let usedVars: ValueSelector[] = [] if (node.data.type === BlockEnum.Tool) { const { provider_type } = node.data @@ -84,8 +90,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { if (provider_type === CollectionType.workflow) toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon } - - if (node.data.type === BlockEnum.Agent) { + else if (node.data.type === BlockEnum.Agent) { const data = node.data as AgentNodeType const isReadyForCheckValid = !!strategyProviders const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name) @@ -97,10 +102,34 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { isReadyForCheckValid, } } + else { + usedVars = getNodeUsedVars(node).filter(v => v.length > 0) + } if (node.type === CUSTOM_NODE) { const checkData = getCheckData(node.data) - const { errorMessage } = nodesExtraData[node.data.type].checkValid(checkData, t, moreDataForCheckValid) + let { errorMessage } = nodesExtraData[node.data.type].checkValid(checkData, t, moreDataForCheckValid) + + if (!errorMessage) { + const availableVars = map[node.id].availableVars + + for (const variable of usedVars) { + const isEnv = isENV(variable) + const isConvVar = isConversationVar(variable) + const isSysVar = isSystemVar(variable) + if (!isEnv && !isConvVar && !isSysVar) { + const usedNode = availableVars.find(v => v.nodeId === variable?.[0]) + if (usedNode) { + const usedVar = usedNode.vars.find(v => v.variable === variable?.[1]) + if (!usedVar) + errorMessage = t('workflow.errorMsg.invalidVariable') + } + else { + errorMessage = t('workflow.errorMsg.invalidVariable') + } + } + } + } if (errorMessage || !validNodes.find(n => n.id === node.id)) { list.push({ diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts new file mode 100644 index 0000000000..27a9ea9d2d --- /dev/null +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -0,0 +1,78 @@ +import { useCallback } from 'react' +import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import { useWorkflowStore } from '@/app/components/workflow/store' +import { useStoreApi } from 'reactflow' +import type { Node } from '@/app/components/workflow/types' +import { fetchAllInspectVars } from '@/service/workflow' +import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow' +import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' + +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} + +export const useSetWorkflowVarsWithValue = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { + const workflowStore = useWorkflowStore() + const store = useStoreApi() + const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl) + const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl) + const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() + + const setInspectVarsToStore = useCallback((inspectVars: VarInInspect[]) => { + const { setNodesWithInspectVars } = workflowStore.getState() + const { getNodes } = store.getState() + const nodeArr = getNodes() + const nodesKeyValue: Record<string, Node> = {} + nodeArr.forEach((node) => { + nodesKeyValue[node.id] = node + }) + + const withValueNodeIds: Record<string, boolean> = {} + inspectVars.forEach((varItem) => { + const nodeId = varItem.selector[0] + + const node = nodesKeyValue[nodeId] + if (!node) + return + withValueNodeIds[nodeId] = true + }) + const withValueNodes = Object.keys(withValueNodeIds).map((nodeId) => { + return nodesKeyValue[nodeId] + }) + + const res: NodeWithVar[] = withValueNodes.map((node) => { + const nodeId = node.id + const varsUnderTheNode = inspectVars.filter((varItem) => { + return varItem.selector[0] === nodeId + }) + const nodeWithVar = { + nodeId, + nodePayload: node.data, + nodeType: node.data.type, + title: node.data.title, + vars: varsUnderTheNode, + isSingRunRunning: false, + isValueFetched: false, + } + return nodeWithVar + }) + setNodesWithInspectVars(res) + }, [workflowStore, store]) + + const fetchInspectVars = useCallback(async () => { + invalidateConversationVarValues() + invalidateSysVarValues() + const data = await fetchAllInspectVars(flowId) + setInspectVarsToStore(data) + handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status + }, [invalidateConversationVarValues, invalidateSysVarValues, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus]) + return { + fetchInspectVars, + } +} diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts new file mode 100644 index 0000000000..ffcfd81666 --- /dev/null +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -0,0 +1,240 @@ +import { fetchNodeInspectVars } from '@/service/workflow' +import { useWorkflowStore } from '@/app/components/workflow/store' +import type { ValueSelector } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import { + useDeleteAllInspectorVars, + useDeleteInspectVar, + useDeleteNodeInspectorVars, + useEditInspectorVar, + useInvalidateConversationVarValues, + useInvalidateSysVarValues, + useResetConversationVar, + useResetToLastRunValue, +} from '@/service/use-workflow' +import { useCallback } from 'react' +import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import produce from 'immer' +import type { Node } from '@/app/components/workflow/types' +import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' +import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' + +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} +export const useInspectVarsCrudCommon = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { + const workflowStore = useWorkflowStore() + const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl!) + const { mutateAsync: doResetConversationVar } = useResetConversationVar(flowId) + const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(flowId) + const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl!) + + const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(flowId) + const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(flowId) + const { mutate: doDeleteInspectVar } = useDeleteInspectVar(flowId) + + const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId) + const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() + const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() + const getNodeInspectVars = useCallback((nodeId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) + return node + }, [workflowStore]) + + const getVarId = useCallback((nodeId: string, varName: string) => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + const varId = node.vars.find((varItem) => { + return varItem.selector[1] === varName + })?.id + return varId + }, [getNodeInspectVars]) + + const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + + const variable = node.vars.find((varItem) => { + return varItem.name === name + }) + return variable + }, [getNodeInspectVars]) + + const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { + const isEnv = isENV([nodeId]) + if (isEnv) // always have value + return true + const isSys = isSystemVar([nodeId]) + if (isSys) + return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) + const isChatVar = isConversationVar([nodeId]) + if (isChatVar) + return conversationVars.some(varItem => varItem.selector?.[1] === name) + return getInspectVar(nodeId, name) !== undefined + }, [getInspectVar]) + + const hasNodeInspectVars = useCallback((nodeId: string) => { + return !!getNodeInspectVars(nodeId) + }, [getNodeInspectVars]) + + const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => { + const { + appId, + setNodeInspectVars, + } = workflowStore.getState() + const nodeId = selector[0] + const isSystemVar = nodeId === 'sys' + const isConversationVar = nodeId === 'conversation' + if (isSystemVar) { + invalidateSysVarValues() + return + } + if (isConversationVar) { + invalidateConversationVarValues() + return + } + const vars = await fetchNodeInspectVars(appId, nodeId) + setNodeInspectVars(nodeId, vars) + }, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues]) + + // after last run would call this + const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { + const { + nodesWithInspectVars, + setNodesWithInspectVars, + } = workflowStore.getState() + const nodes = produce(nodesWithInspectVars, (draft) => { + const nodeInfo = allNodes.find(node => node.id === nodeId) + if (nodeInfo) { + const index = draft.findIndex(node => node.nodeId === nodeId) + if (index === -1) { + draft.unshift({ + nodeId, + nodeType: nodeInfo.data.type, + title: nodeInfo.data.title, + vars: payload, + nodePayload: nodeInfo.data, + }) + } + else { + draft[index].vars = payload + // put the node to the topAdd commentMore actions + draft.unshift(draft.splice(index, 1)[0]) + } + } + }) + setNodesWithInspectVars(nodes) + handleCancelNodeSuccessStatus(nodeId) + }, [workflowStore, handleCancelNodeSuccessStatus]) + + const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) + if(!targetNode || !targetNode.vars) + return false + return targetNode.vars.some(item => item.id === varId) + }, [workflowStore]) + + const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => { + const { deleteInspectVar } = workflowStore.getState() + if(hasNodeInspectVar(nodeId, varId)) { + await doDeleteInspectVar(varId) + deleteInspectVar(nodeId, varId) + } + }, [doDeleteInspectVar, workflowStore, hasNodeInspectVar]) + + const resetConversationVar = useCallback(async (varId: string) => { + await doResetConversationVar(varId) + invalidateConversationVarValues() + }, [doResetConversationVar, invalidateConversationVarValues]) + + const deleteNodeInspectorVars = useCallback(async (nodeId: string) => { + const { deleteNodeInspectVars } = workflowStore.getState() + if (hasNodeInspectVars(nodeId)) { + await doDeleteNodeInspectorVars(nodeId) + deleteNodeInspectVars(nodeId) + } + }, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars]) + + const deleteAllInspectorVars = useCallback(async () => { + const { deleteAllInspectVars } = workflowStore.getState() + await doDeleteAllInspectorVars() + await invalidateConversationVarValues() + await invalidateSysVarValues() + deleteAllInspectVars() + handleEdgeCancelRunningStatus() + }, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus]) + + const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { + const { setInspectVarValue } = workflowStore.getState() + await doEditInspectorVar({ + varId, + value, + }) + setInspectVarValue(nodeId, varId, value) + if (nodeId === VarInInspectType.conversation) + invalidateConversationVarValues() + if (nodeId === VarInInspectType.system) + invalidateSysVarValues() + }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore]) + + const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => { + const { renameInspectVarName } = workflowStore.getState() + const varId = getVarId(nodeId, oldName) + if (!varId) + return + + const newSelector = [nodeId, newName] + await doEditInspectorVar({ + varId, + name: newName, + }) + renameInspectVarName(nodeId, varId, newSelector) + }, [doEditInspectorVar, getVarId, workflowStore]) + + const isInspectVarEdited = useCallback((nodeId: string, name: string) => { + const inspectVar = getInspectVar(nodeId, name) + if (!inspectVar) + return false + + return inspectVar.edited + }, [getInspectVar]) + + const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => { + const { resetToLastRunVar } = workflowStore.getState() + const isSysVar = nodeId === 'sys' + const data = await doResetToLastRunValue(varId) + + if(isSysVar) + invalidateSysVarValues() + else + resetToLastRunVar(nodeId, varId, data.value) + }, [doResetToLastRunValue, invalidateSysVarValues, workflowStore]) + + return { + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } +} diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts new file mode 100644 index 0000000000..50188185c2 --- /dev/null +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts @@ -0,0 +1,49 @@ +import { useStore } from '../store' +import { useHooksStore } from '@/app/components/workflow/hooks-store' +import { + useConversationVarValues, + useSysVarValues, +} from '@/service/use-workflow' + +const useInspectVarsCrud = () => { + const nodesWithInspectVars = useStore(s => s.nodesWithInspectVars) + const configsMap = useHooksStore(s => s.configsMap) + const { data: conversationVars } = useConversationVarValues(configsMap?.conversationVarsUrl) + const { data: systemVars } = useSysVarValues(configsMap?.systemVarsUrl) + const hasNodeInspectVars = useHooksStore(s => s.hasNodeInspectVars) + const hasSetInspectVar = useHooksStore(s => s.hasSetInspectVar) + const fetchInspectVarValue = useHooksStore(s => s.fetchInspectVarValue) + const editInspectVarValue = useHooksStore(s => s.editInspectVarValue) + const renameInspectVarName = useHooksStore(s => s.renameInspectVarName) + const appendNodeInspectVars = useHooksStore(s => s.appendNodeInspectVars) + const deleteInspectVar = useHooksStore(s => s.deleteInspectVar) + const deleteNodeInspectorVars = useHooksStore(s => s.deleteNodeInspectorVars) + const deleteAllInspectorVars = useHooksStore(s => s.deleteAllInspectorVars) + const isInspectVarEdited = useHooksStore(s => s.isInspectVarEdited) + const resetToLastRunVar = useHooksStore(s => s.resetToLastRunVar) + const invalidateSysVarValues = useHooksStore(s => s.invalidateSysVarValues) + const resetConversationVar = useHooksStore(s => s.resetConversationVar) + const invalidateConversationVarValues = useHooksStore(s => s.invalidateConversationVarValues) + + return { + conversationVars: conversationVars || [], + systemVars: systemVars || [], + nodesWithInspectVars, + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } +} + +export default useInspectVarsCrud diff --git a/web/app/components/workflow/hooks/use-nodes-available-var-list.ts b/web/app/components/workflow/hooks/use-nodes-available-var-list.ts new file mode 100644 index 0000000000..5efe2519ef --- /dev/null +++ b/web/app/components/workflow/hooks/use-nodes-available-var-list.ts @@ -0,0 +1,75 @@ +import { + useIsChatMode, + useWorkflow, + useWorkflowVariables, +} from '@/app/components/workflow/hooks' +import { BlockEnum, type Node, type NodeOutPutVar, type ValueSelector, type Var } from '@/app/components/workflow/types' +type Params = { + onlyLeafNodeVar?: boolean + hideEnv?: boolean + hideChatVar?: boolean + filterVar: (payload: Var, selector: ValueSelector) => boolean + passedInAvailableNodes?: Node[] +} + +const getNodeInfo = (nodeId: string, nodes: Node[]) => { + const allNodes = nodes + const node = allNodes.find(n => n.id === nodeId) + const isInIteration = !!node?.data.isInIteration + const isInLoop = !!node?.data.isInLoop + const parentNodeId = node?.parentId + const parentNode = allNodes.find(n => n.id === parentNodeId) + return { + node, + isInIteration, + isInLoop, + parentNode, + } +} + +// TODO: loop type? +const useNodesAvailableVarList = (nodes: Node[], { + onlyLeafNodeVar, + filterVar, + hideEnv = false, + hideChatVar = false, + passedInAvailableNodes, +}: Params = { + onlyLeafNodeVar: false, + filterVar: () => true, + }) => { + const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() + const { getNodeAvailableVars } = useWorkflowVariables() + const isChatMode = useIsChatMode() + + const nodeAvailabilityMap: { [key: string ]: { availableVars: NodeOutPutVar[], availableNodes: Node[] } } = {} + + nodes.forEach((node) => { + const nodeId = node.id + const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId)) + if (node.data.type === BlockEnum.Loop) + availableNodes.push(node) + + const { + parentNode: iterationNode, + } = getNodeInfo(nodeId, nodes) + + const availableVars = getNodeAvailableVars({ + parentNode: iterationNode, + beforeNodes: availableNodes, + isChatMode, + filterVar, + hideEnv, + hideChatVar, + }) + const result = { + node, + availableVars, + availableNodes, + } + nodeAvailabilityMap[nodeId] = result + }) + return nodeAvailabilityMap +} + +export default useNodesAvailableVarList diff --git a/web/app/components/workflow/hooks/use-nodes-data.ts b/web/app/components/workflow/hooks/use-nodes-data.ts index aeb45ddb93..7df6b2ffd0 100644 --- a/web/app/components/workflow/hooks/use-nodes-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-data.ts @@ -34,15 +34,14 @@ export const useNodesExtraData = () => { export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean, isInLoop?: boolean) => { const nodesExtraData = useNodesExtraData() const availablePrevBlocks = useMemo(() => { - if (!nodeType) + if (!nodeType || !nodesExtraData[nodeType]) return [] return nodesExtraData[nodeType].availablePrevNodes || [] }, [nodeType, nodesExtraData]) const availableNextBlocks = useMemo(() => { - if (!nodeType) + if (!nodeType || !nodesExtraData[nodeType]) return [] - return nodesExtraData[nodeType].availableNextNodes || [] }, [nodeType, nodesExtraData]) @@ -55,10 +54,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) return false - if (!isInLoop && nType === BlockEnum.LoopEnd) - return false - - return true + return !(!isInLoop && nType === BlockEnum.LoopEnd) }), availableNextBlocks: availableNextBlocks.filter((nType) => { if (isInIteration && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) @@ -67,10 +63,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) return false - if (!isInLoop && nType === BlockEnum.LoopEnd) - return false - - return true + return !(!isInLoop && nType === BlockEnum.LoopEnd) }), } }, [isInIteration, availablePrevBlocks, availableNextBlocks, isInLoop]) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts index 7fbf0ce868..e01609cdb6 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react' import produce from 'immer' import { useStoreApi } from 'reactflow' +import { NodeRunningStatus } from '../types' export const useNodesInteractionsWithoutSync = () => { const store = useStoreApi() @@ -21,7 +22,41 @@ export const useNodesInteractionsWithoutSync = () => { setNodes(newNodes) }, [store]) + const handleCancelAllNodeSuccessStatus = useCallback(() => { + const { + getNodes, + setNodes, + } = store.getState() + + const nodes = getNodes() + const newNodes = produce(nodes, (draft) => { + draft.forEach((node) => { + if(node.data._runningStatus === NodeRunningStatus.Succeeded) + node.data._runningStatus = undefined + }) + }) + setNodes(newNodes) + }, [store]) + + const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => { + const { + getNodes, + setNodes, + } = store.getState() + + const newNodes = produce(getNodes(), (draft) => { + const node = draft.find(n => n.id === nodeId) + if (node && node.data._runningStatus === NodeRunningStatus.Succeeded) { + node.data._runningStatus = undefined + node.data._waitingRun = false + } + }) + setNodes(newNodes) + }, [store]) + return { handleNodeCancelRunningStatus, + handleCancelAllNodeSuccessStatus, + handleCancelNodeSuccessStatus, } } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 94b10c9929..b598951adb 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -60,6 +60,7 @@ import { useWorkflowReadOnly, } from './use-workflow' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' +import useInspectVarsCrud from './use-inspect-vars-crud' export const useNodesInteractions = () => { const { t } = useTranslation() @@ -288,7 +289,9 @@ export const useNodesInteractions = () => { setEdges(newEdges) }, [store, workflowStore, getNodesReadOnly]) - const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => { + const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean, initShowLastRunTab?: boolean) => { + if(initShowLastRunTab) + workflowStore.setState({ initShowLastRunTab: true }) const { getNodes, setNodes, @@ -530,6 +533,8 @@ export const useNodesInteractions = () => { setEnteringNodePayload(undefined) }, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow]) + const { deleteNodeInspectorVars } = useInspectVarsCrud() + const handleNodeDelete = useCallback((nodeId: string) => { if (getNodesReadOnly()) return @@ -551,6 +556,7 @@ export const useNodesInteractions = () => { if (currentNode.data.type === BlockEnum.Start) return + deleteNodeInspectorVars(nodeId) if (currentNode.data.type === BlockEnum.Iteration) { const iterationChildren = nodes.filter(node => node.parentId === currentNode.id) @@ -655,7 +661,7 @@ export const useNodesInteractions = () => { else saveStateToHistory(WorkflowHistoryEvent.NodeDelete) - }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) + }, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) const handleNodeAdd = useCallback<OnNodeAdd>(( { diff --git a/web/app/components/workflow/hooks/use-set-workflow-vars-with-value.ts b/web/app/components/workflow/hooks/use-set-workflow-vars-with-value.ts new file mode 100644 index 0000000000..a04c2de305 --- /dev/null +++ b/web/app/components/workflow/hooks/use-set-workflow-vars-with-value.ts @@ -0,0 +1,9 @@ +import { useHooksStore } from '@/app/components/workflow/hooks-store' + +export const useSetWorkflowVarsWithValue = () => { + const fetchInspectVars = useHooksStore(s => s.fetchInspectVars) + + return { + fetchInspectVars, + } +} diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index 8b1003e89c..118ec94058 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -11,6 +11,7 @@ import { useEdgesInteractions, useNodesInteractions, useNodesSyncDraft, + useWorkflowCanvasMaximize, useWorkflowMoveMode, useWorkflowOrganize, useWorkflowStartRun, @@ -35,6 +36,7 @@ export const useShortcuts = (): void => { handleModePointer, } = useWorkflowMoveMode() const { handleLayout } = useWorkflowOrganize() + const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize() const { zoomTo, @@ -145,6 +147,16 @@ export const useShortcuts = (): void => { } }, { exactMatch: true, useCapture: true }) + useKeyPress('f', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleToggleMaximizeCanvas() + } + }, { + exactMatch: true, + useCapture: true, + }) + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 636d3b94f9..d8653a5942 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -401,3 +401,29 @@ export const useDSL = () => { handleExportDSL, } } + +export const useWorkflowCanvasMaximize = () => { + const { eventEmitter } = useEventEmitterContextContext() + + const maximizeCanvas = useStore(s => s.maximizeCanvas) + const setMaximizeCanvas = useStore(s => s.setMaximizeCanvas) + const { + getNodesReadOnly, + } = useNodesReadOnly() + + const handleToggleMaximizeCanvas = useCallback(() => { + if (getNodesReadOnly()) + return + + setMaximizeCanvas(!maximizeCanvas) + localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas)) + eventEmitter?.emit({ + type: 'workflow-canvas-maximize', + payload: !maximizeCanvas, + } as any) + }, [eventEmitter, getNodesReadOnly, maximizeCanvas, setMaximizeCanvas]) + + return { + handleToggleMaximizeCanvas, + } +} diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 99dce4dc15..8bc9d3436f 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { fetchAllBuiltInTools, fetchAllCustomTools, + fetchAllMCPTools, fetchAllWorkflowTools, } from '@/service/tools' import { CollectionType } from '@/app/components/tools/types' @@ -59,10 +60,6 @@ export const useWorkflow = () => { const store = useStoreApi() const workflowStore = useWorkflowStore() const nodesExtraData = useNodesExtraData() - const setPanelWidth = useCallback((width: number) => { - localStorage.setItem('workflow-node-panel-width', `${width}`) - workflowStore.setState({ panelWidth: width }) - }, [workflowStore]) const getTreeLeafNodes = useCallback((nodeId: string) => { const { @@ -399,7 +396,6 @@ export const useWorkflow = () => { }, [store]) return { - setPanelWidth, getTreeLeafNodes, getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent, @@ -450,6 +446,13 @@ export const useFetchToolsData = () => { workflowTools: workflowTools || [], }) } + if(type === 'mcp') { + const mcpTools = await fetchAllMCPTools() + + workflowStore.setState({ + mcpTools: mcpTools || [], + }) + } }, [workflowStore]) return { @@ -496,18 +499,24 @@ export const useToolIcon = (data: Node['data']) => { const buildInTools = useStore(s => s.buildInTools) const customTools = useStore(s => s.customTools) const workflowTools = useStore(s => s.workflowTools) + const mcpTools = useStore(s => s.mcpTools) + const toolIcon = useMemo(() => { + if(!data) + return '' if (data.type === BlockEnum.Tool) { let targetTools = buildInTools if (data.provider_type === CollectionType.builtIn) targetTools = buildInTools else if (data.provider_type === CollectionType.custom) targetTools = customTools + else if (data.provider_type === CollectionType.mcp) + targetTools = mcpTools else targetTools = workflowTools return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon } - }, [data, buildInTools, customTools, workflowTools]) + }, [data, buildInTools, customTools, mcpTools, workflowTools]) return toolIcon } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 549117faf7..8ea861ebb4 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -5,6 +5,7 @@ import { memo, useCallback, useEffect, + useMemo, useRef, } from 'react' import { setAutoFreeze } from 'immer' @@ -41,6 +42,7 @@ import { useNodesSyncDraft, usePanelInteractions, useSelectionInteractions, + useSetWorkflowVarsWithValue, useShortcuts, useWorkflow, useWorkflowReadOnly, @@ -56,6 +58,7 @@ import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' import CustomSimpleNode from './simple-node' import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' import Operator from './operator' +import Control from './operator/control' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' import HelpLine from './help-line' @@ -114,6 +117,32 @@ export const Workflow: FC<WorkflowProps> = memo(({ const controlMode = useStore(s => s.controlMode) const nodeAnimation = useStore(s => s.nodeAnimation) const showConfirm = useStore(s => s.showConfirm) + const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight) + const bottomPanelHeight = useStore(s => s.bottomPanelHeight) + const setWorkflowCanvasWidth = useStore(s => s.setWorkflowCanvasWidth) + const setWorkflowCanvasHeight = useStore(s => s.setWorkflowCanvasHeight) + const controlHeight = useMemo(() => { + if (!workflowCanvasHeight) + return '100%' + return workflowCanvasHeight - bottomPanelHeight + }, [workflowCanvasHeight, bottomPanelHeight]) + + // update workflow Canvas width and height + useEffect(() => { + if (workflowContainerRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize, blockSize } = entry.borderBoxSize[0] + setWorkflowCanvasWidth(inlineSize) + setWorkflowCanvasHeight(blockSize) + } + }) + resizeContainerObserver.observe(workflowContainerRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setWorkflowCanvasHeight, setWorkflowCanvasWidth]) const { setShowConfirm, @@ -205,6 +234,7 @@ export const Workflow: FC<WorkflowProps> = memo(({ handleFetchAllTools('builtin') handleFetchAllTools('custom') handleFetchAllTools('workflow') + handleFetchAllTools('mcp') }, [handleFetchAllTools]) const { @@ -245,6 +275,11 @@ export const Workflow: FC<WorkflowProps> = memo(({ }) useShortcuts() + const { fetchInspectVars } = useSetWorkflowVarsWithValue() + useEffect(() => { + fetchInspectVars() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const store = useStoreApi() if (process.env.NODE_ENV === 'development') { @@ -267,6 +302,12 @@ export const Workflow: FC<WorkflowProps> = memo(({ > <SyncingDataModal /> <CandidateNode /> + <div + className='absolute left-0 top-0 z-10 flex w-12 items-center justify-center p-1 pl-2' + style={{ height: controlHeight }} + > + <Control /> + </div> <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> <PanelContextmenu /> <NodeContextmenu /> diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index dd6a1c6a22..c872952900 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -22,6 +22,7 @@ import type { ListRef } from '@/app/components/workflow/block-selector/market-pl import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list' import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks' import { ToolTipContent } from '@/app/components/base/tooltip/content' +import { useGlobalPublicStore } from '@/context/global-public-context' const DEFAULT_TAGS: ListProps['tags'] = [] @@ -48,7 +49,6 @@ const NotFoundWarn = (props: { </p> </div> } - needsDelay > <div> <RiErrorWarningFill className='size-4 text-text-destructive' /> @@ -67,6 +67,7 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s icon: getIcon(item.declaration.identity.icon), label: item.declaration.identity.label as any, type: CollectionType.all, + meta: item.meta, tools: item.declaration.strategies.map(strategy => ({ name: strategy.identity.name, author: strategy.identity.author, @@ -88,10 +89,13 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s export type AgentStrategySelectorProps = { value?: Strategy, onChange: (value?: Strategy) => void, + canChooseMCPTool: boolean, } export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => { - const { value, onChange } = props + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + + const { value, onChange, canChooseMCPTool } = props const [open, setOpen] = useState(false) const [viewType, setViewType] = useState<ViewType>(ViewType.flat) const [query, setQuery] = useState('') @@ -132,6 +136,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => } = useMarketplacePlugins() useEffect(() => { + if (!enable_marketplace) return if (query) { fetchPlugins({ query, @@ -158,7 +163,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => alt='icon' /></div>} <p - className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'text-xs px-1')} + className={classNames(value ? 'text-components-input-text-filled' : 'text-components-input-text-placeholder', 'px-1 text-xs')} > {value?.agent_strategy_label || t('workflow.nodes.agent.strategy.selectTip')} </p> @@ -210,19 +215,25 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => agent_strategy_label: tool!.tool_label, agent_output_schema: tool!.output_schema, plugin_unique_identifier: tool!.provider_id, + meta: tool!.meta, }) setOpen(false) }} className='h-full max-h-full max-w-none overflow-y-auto' - indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} /> - <PluginList + indexBarClassName='top-0 xl:top-36' + hasSearchText={false} + canNotSelectMultiple + canChooseMCPTool={canChooseMCPTool} + isAgent + /> + {enable_marketplace && <PluginList ref={pluginRef} wrapElemRef={wrapElemRef} list={notInstalledPlugins} searchText={query} tags={DEFAULT_TAGS} disableMaxWidth - /> + />} </main> </div> </PortalToFollowElemContent> diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index de23602e34..ce9fbb77e1 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -19,9 +19,9 @@ import { useWorkflowStore } from '../../../store' import { useRenderI18nObject } from '@/hooks/use-i18n' import type { NodeOutPutVar } from '../../../types' import type { Node } from 'reactflow' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import type { PluginMeta } from '@/app/components/plugins/types' +import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' export type Strategy = { agent_strategy_provider_name: string @@ -29,6 +29,7 @@ export type Strategy = { agent_strategy_label: string agent_output_schema: Record<string, any> plugin_unique_identifier: string + meta?: PluginMeta } export type AgentStrategyProps = { @@ -40,6 +41,7 @@ export type AgentStrategyProps = { nodeOutputVars?: NodeOutPutVar[], availableNodes?: Node[], nodeId?: string + canChooseMCPTool: boolean } type CustomSchema<Type, Field = {}> = Omit<CredentialFormSchema, 'type'> & { type: Type } & Field @@ -50,15 +52,16 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'> type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema export const AgentStrategy = memo((props: AgentStrategyProps) => { - const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props + const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) const renderI18nObject = useRenderI18nObject() const workflowStore = useWorkflowStore() const { setControlPromptEditorRerenderKey, } = workflowStore.getState() + const override: ComponentProps<typeof Form<CustomField>>['override'] = [ [FormTypeEnum.textNumber, FormTypeEnum.textInput], (schema, props) => { @@ -120,6 +123,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { title={<> {renderI18nObject(def.label)} {def.required && <span className='text-red-500'>*</span>} </>} + key={def.variable} tooltip={def.tooltip && renderI18nObject(def.tooltip)} inline > @@ -169,6 +173,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { value={value} onSelect={item => onChange(item)} onDelete={() => onChange(null)} + canChooseMCPTool={canChooseMCPTool} + onSelectMultiple={noop} /> </Field> ) @@ -190,13 +196,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { onChange={onChange} supportCollapse required={schema.required} + canChooseMCPTool={canChooseMCPTool} /> ) } } } return <div className='space-y-2'> - <AgentStrategySelector value={strategy} onChange={onStrategyChange} /> + <AgentStrategySelector value={strategy} onChange={onStrategyChange} canChooseMCPTool={canChooseMCPTool} /> { strategy ? <div> @@ -216,6 +223,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { nodeId={nodeId} nodeOutputVars={nodeOutputVars || []} availableNodes={availableNodes || []} + canChooseMCPTool={canChooseMCPTool} /> </div> : <ListEmpty @@ -223,11 +231,11 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { title={t('workflow.nodes.agent.strategy.configureTip')} description={<div className='text-xs text-text-tertiary'> {t('workflow.nodes.agent.strategy.configureTipDesc')} <br /> - <Link href={ - locale === LanguagesSupported[1] - ? 'https://docs.dify.ai/zh-hans/guides/workflow/node/agent#xuan-ze-agent-ce-le' - : 'https://docs.dify.ai/en/guides/workflow/node/agent#select-an-agent-strategy' - } className='text-text-accent-secondary' target='_blank'> + <Link href={docLink('/guides/workflow/node/agent#select-an-agent-strategy', { + 'zh-Hans': '/guides/workflow/node/agent#选择-agent-策略', + 'ja-JP': '/guides/workflow/node/agent#エージェント戦略の選択', + })} + className='text-text-accent-secondary' target='_blank'> {t('workflow.nodes.agent.learnMore')} </Link> </div>} diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx index 9f415adda9..269f5e0a96 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx @@ -95,6 +95,7 @@ const FormItem: FC<Props> = ({ const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type) const isContext = type === InputVarType.contexts const isIterator = type === InputVarType.iterator + const isIteratorItemFile = isIterator && payload.isFileItem const singleFileValue = useMemo(() => { if (payload.variable === '#files#') return value?.[0] || [] @@ -202,12 +203,12 @@ const FormItem: FC<Props> = ({ }} /> )} - {(type === InputVarType.multiFiles) && ( + {(type === InputVarType.multiFiles || isIteratorItemFile) && ( <FileUploaderInAttachmentWrapper value={value} onChange={files => onChange(files)} fileConfig={{ - allowed_file_types: inStepRun + allowed_file_types: (inStepRun || isIteratorItemFile) ? [ SupportUploadFileTypes.image, SupportUploadFileTypes.document, @@ -215,7 +216,7 @@ const FormItem: FC<Props> = ({ SupportUploadFileTypes.video, ] : payload.allowed_file_types, - allowed_file_extensions: inStepRun + allowed_file_extensions: (inStepRun || isIteratorItemFile) ? [ ...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.document], @@ -223,8 +224,8 @@ const FormItem: FC<Props> = ({ ...FILE_EXTS[SupportUploadFileTypes.video], ] : payload.allowed_file_extensions, - allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods, - number_limits: inStepRun ? 5 : payload.max_length, + allowed_file_upload_methods: (inStepRun || isIteratorItemFile) ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods, + number_limits: (inStepRun || isIteratorItemFile) ? 5 : payload.max_length, fileUploadConfig: fileSettings?.fileUploadConfig, }} /> @@ -272,7 +273,7 @@ const FormItem: FC<Props> = ({ } { - isIterator && ( + (isIterator && !isIteratorItemFile) && ( <div className='space-y-2'> {(value || []).map((item: any, index: number) => ( <TextEditor diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx index 884729c39a..780a01d89b 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx @@ -61,10 +61,14 @@ const Form: FC<Props> = ({ } }, [valuesRef, onChange, mapKeysWithSameValueSelector]) const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type) + const isIteratorItemFile = inputs[0]?.type === InputVarType.iterator && inputs[0]?.isFileItem + const isContext = inputs[0]?.type === InputVarType.contexts const handleAddContext = useCallback(() => { const newValues = produce(values, (draft: any) => { const key = inputs[0].variable + if (!draft[key]) + draft[key] = [] draft[key].push(isContext ? RETRIEVAL_OUTPUT_STRUCT : '') }) onChange(newValues) @@ -75,7 +79,7 @@ const Form: FC<Props> = ({ {label && ( <div className='mb-1 flex items-center justify-between'> <div className='system-xs-medium-uppercase flex h-6 items-center text-text-tertiary'>{label}</div> - {isArrayLikeType && ( + {isArrayLikeType && !isIteratorItemFile && ( <AddButton onClick={handleAddContext} /> )} </div> diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index ad8d0b9c61..11bd5156ef 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -1,30 +1,23 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { - RiCloseLine, - RiLoader2Line, -} from '@remixicon/react' import type { Props as FormProps } from './form' import Form from './form' import cn from '@/utils/classnames' import Button from '@/app/components/base/button' -import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' import Split from '@/app/components/workflow/nodes/_base/components/split' -import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import { InputVarType } from '@/app/components/workflow/types' import Toast from '@/app/components/base/toast' import { TransferMethod } from '@/types/app' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' -import type { BlockEnum } from '@/app/components/workflow/types' +import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import type { Emoji } from '@/app/components/tools/types' import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel' -import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' - +import PanelWrap from './panel-wrap' const i18nPrefix = 'workflow.singleRun' -type BeforeRunFormProps = { +export type BeforeRunFormProps = { nodeName: string nodeType?: BlockEnum toolIcon?: string | Emoji @@ -32,12 +25,15 @@ type BeforeRunFormProps = { onRun: (submitData: Record<string, any>) => void onStop: () => void runningStatus: NodeRunningStatus - result?: React.JSX.Element forms: FormProps[] showSpecialResultPanel?: boolean + existVarValuesInForms: Record<string, any>[] + filteredExistVarForms: FormProps[] } & Partial<SpecialResultPanelProps> function formatValue(value: string | any, type: InputVarType) { + if(value === undefined || value === null) + return value if (type === InputVarType.number) return Number.parseFloat(value) if (type === InputVarType.json) @@ -53,6 +49,8 @@ function formatValue(value: string | any, type: InputVarType) { if (type === InputVarType.singleFile) { if (Array.isArray(value)) return getProcessedFiles(value) + if (!value) + return undefined return getProcessedFiles([value])[0] } @@ -60,22 +58,17 @@ function formatValue(value: string | any, type: InputVarType) { } const BeforeRunForm: FC<BeforeRunFormProps> = ({ nodeName, - nodeType, - toolIcon, onHide, onRun, - onStop, - runningStatus, - result, forms, - showSpecialResultPanel, - ...restResultPanelParams + filteredExistVarForms, + existVarValuesInForms, }) => { const { t } = useTranslation() - const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception - const isRunning = runningStatus === NodeRunningStatus.Running const isFileLoaded = (() => { + if (!forms || forms.length === 0) + return true // system files const filesForm = forms.find(item => !!item.values['#files#']) if (!filesForm) @@ -87,12 +80,14 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({ return true })() - const handleRun = useCallback(() => { + const handleRun = () => { let errMsg = '' - forms.forEach((form) => { + forms.forEach((form, i) => { + const existVarValuesInForm = existVarValuesInForms[i] + form.inputs.forEach((input) => { const value = form.values[input.variable] as any - if (!errMsg && input.required && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0))) + if (!errMsg && input.required && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0))) errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label }) if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) { @@ -137,69 +132,45 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({ } onRun(submitData) - }, [forms, onRun, t]) + } + const hasRun = useRef(false) + useEffect(() => { + // React 18 run twice in dev mode + if(hasRun.current) + return + hasRun.current = true + if(filteredExistVarForms.length === 0) + onRun({}) + }, [filteredExistVarForms, onRun]) + + if(filteredExistVarForms.length === 0) + return null + return ( - <div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt pt-10'> - <div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'> - <div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'> - <div className='truncate text-base font-semibold text-text-primary'> - {t(`${i18nPrefix}.testRun`)} {nodeName} - </div> - <div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => { - onHide() - }}> - <RiCloseLine className='h-4 w-4 text-text-tertiary ' /> - </div> - </div> - { - showSpecialResultPanel && ( - <div className='h-0 grow overflow-y-auto pb-4'> - <SpecialResultPanel {...restResultPanelParams} /> - </div> - ) - } - { - !showSpecialResultPanel && ( - <div className='h-0 grow overflow-y-auto pb-4'> - <div className='mt-3 space-y-4 px-4'> - {forms.map((form, index) => ( - <div key={index}> - <Form - key={index} - className={cn(index < forms.length - 1 && 'mb-4')} - {...form} - /> - {index < forms.length - 1 && <Split />} - </div> - ))} - </div> - <div className='mt-4 flex justify-between space-x-2 px-4' > - {isRunning && ( - <div - className='cursor-pointer rounded-lg border border-divider-regular bg-components-button-secondary-bg p-2 shadow-xs' - onClick={onStop} - > - <StopCircle className='h-4 w-4 text-text-tertiary' /> - </div> - )} - <Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}> - {isRunning && <RiLoader2Line className='h-4 w-4 animate-spin' />} - <div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div> - </Button> - </div> - {isRunning && ( - <ResultPanel status='running' showSteps={false} /> - )} - {isFinished && ( - <> - {result} - </> - )} + <PanelWrap + nodeName={nodeName} + onHide={onHide} + > + <div className='h-0 grow overflow-y-auto pb-4'> + <div className='mt-3 space-y-4 px-4'> + {filteredExistVarForms.map((form, index) => ( + <div key={index}> + <Form + key={index} + className={cn(index < forms.length - 1 && 'mb-4')} + {...form} + /> + {index < forms.length - 1 && <Split />} </div> - ) - } + ))} + </div> + <div className='mt-4 flex justify-between space-x-2 px-4' > + <Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}> + <div>{t(`${i18nPrefix}.startRun`)}</div> + </Button> + </div> </div> - </div> + </PanelWrap> ) } export default React.memo(BeforeRunForm) diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx new file mode 100644 index 0000000000..7312adf6c6 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx @@ -0,0 +1,41 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, +} from '@remixicon/react' + +const i18nPrefix = 'workflow.singleRun' + +export type Props = { + nodeName: string + onHide: () => void + children: React.ReactNode +} + +const PanelWrap: FC<Props> = ({ + nodeName, + onHide, + children, +}) => { + const { t } = useTranslation() + return ( + <div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt'> + <div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'> + <div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'> + <div className='truncate text-base font-semibold text-text-primary'> + {t(`${i18nPrefix}.testRun`)} {nodeName} + </div> + <div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => { + onHide() + }}> + <RiCloseLine className='h-4 w-4 text-text-tertiary ' /> + </div> + </div> + {children} + </div> + </div> + ) +} +export default React.memo(PanelWrap) diff --git a/web/app/components/workflow/nodes/_base/components/editor/base.tsx b/web/app/components/workflow/nodes/_base/components/editor/base.tsx index 38968b2e0d..284c6c77eb 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/base.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/base.tsx @@ -15,6 +15,7 @@ import { import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend' import type { FileEntity } from '@/app/components/base/file-uploader/types' import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log' +import ActionButton from '@/app/components/base/action-button' type Props = { className?: string @@ -75,7 +76,7 @@ const Base: FC<Props> = ({ return ( <Wrap className={cn(wrapClassName)} style={wrapStyle} isInNode={isInNode} isExpand={isExpand}> - <div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}> + <div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', !isFocus ? 'border-transparent bg-components-input-bg-normal' : 'overflow-hidden border-components-input-border-hover bg-components-input-bg-hover')}> <div className='flex h-7 items-center justify-between pl-3 pr-2 pt-1'> <div className='system-xs-semibold-uppercase text-text-secondary'>{title}</div> <div className='flex items-center' onClick={(e) => { @@ -88,15 +89,16 @@ const Base: FC<Props> = ({ <CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages} /> </div> )} - {!isCopied - ? ( - <Clipboard className='mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary' onClick={handleCopy} /> - ) - : ( - <ClipboardCheck className='mx-1 h-3.5 w-3.5 text-text-tertiary' /> - ) - } - + <ActionButton className='ml-1' onClick={handleCopy}> + {!isCopied + ? ( + <Clipboard className='h-4 w-4 cursor-pointer' /> + ) + : ( + <ClipboardCheck className='h-4 w-4' /> + ) + } + </ActionButton> <div className='ml-1'> <ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} /> </div> diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 1e195c5d40..748698747c 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -23,7 +23,7 @@ export type Props = { value?: string | object placeholder?: React.JSX.Element | string onChange?: (value: string) => void - title?: React.JSX.Element + title?: string | React.JSX.Element language: CodeLanguage headerRight?: React.JSX.Element readOnly?: boolean @@ -140,6 +140,7 @@ const CodeEditor: FC<Props> = ({ language={languageMap[language] || 'javascript'} theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme value={outPutValue} + loading={<span className='text-text-primary'>Loading...</span>} onChange={handleEditorChange} // https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html options={{ diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx index 51969f8510..f9292be477 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx @@ -5,6 +5,7 @@ import Input from '@/app/components/base/input' import { VarType } from '@/app/components/workflow/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { useDocLink } from '@/context/i18n' type DefaultValueProps = { forms: DefaultValueForm[] @@ -15,6 +16,7 @@ const DefaultValue = ({ onFormChange, }: DefaultValueProps) => { const { t } = useTranslation() + const docLink = useDocLink() const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => { return (payload: any) => { let value @@ -34,7 +36,9 @@ const DefaultValue = ({ {t('workflow.nodes.common.errorHandle.defaultValue.desc')}   <a - href='https://docs.dify.ai/en/guides/workflow/error-handling/README' + href={docLink('/guides/workflow/error-handling/README', { + 'zh-Hans': '/guides/workflow/error-handling/readme', + })} target='_blank' className='text-text-accent' > diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx index 05a6cb96af..fa9cff3dc8 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx @@ -1,8 +1,10 @@ import { RiMindMap } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' const FailBranchCard = () => { const { t } = useTranslation() + const docLink = useDocLink() return ( <div className='px-4 pt-2'> @@ -17,7 +19,7 @@ const FailBranchCard = () => { {t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}   <a - href='https://docs.dify.ai/guides/workflow/error-handling' + href={docLink('/guides/workflow/error-handling/error-type')} target='_blank' className='text-text-accent' > diff --git a/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx new file mode 100644 index 0000000000..07c3a087b9 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx @@ -0,0 +1,35 @@ +'use client' +import type { FC } from 'react' +import cn from '@/utils/classnames' + +type Props = { + value: boolean + onChange: (value: boolean) => void +} + +const FormInputBoolean: FC<Props> = ({ + value, + onChange, +}) => { + return ( + <div className='flex w-full space-x-1'> + <div + className={cn( + 'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary', + !value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', + value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs', + )} + onClick={() => onChange(true)} + >True</div> + <div + className={cn( + 'system-sm-regular flex h-8 grow cursor-default items-center justify-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary', + value && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', + !value && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs', + )} + onClick={() => onChange(false)} + >False</div> + </div> + ) +} +export default FormInputBoolean diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx new file mode 100644 index 0000000000..316a5c9819 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -0,0 +1,279 @@ +'use client' +import type { FC } from 'react' +import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { VarType } from '@/app/components/workflow/types' + +import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' +import FormInputTypeSwitch from './form-input-type-switch' +import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' +import Input from '@/app/components/base/input' +import { SimpleSelect } from '@/app/components/base/select' +import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input' +import FormInputBoolean from './form-input-boolean' +import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' +import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' +import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import cn from '@/utils/classnames' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema + value: ToolVarInputs + onChange: (value: any) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const FormInputItem: FC<Props> = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + const language = useLanguage() + + const { + placeholder, + variable, + type, + default: defaultValue, + options, + scope, + } = schema as any + const varInput = value[variable] + const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput + const isNumber = type === FormTypeEnum.textNumber + const isObject = type === FormTypeEnum.object + const isArray = type === FormTypeEnum.array + const isShowJSONEditor = isObject || isArray + const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files + const isBoolean = type === FormTypeEnum.boolean + const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect + const isAppSelector = type === FormTypeEnum.appSelector + const isModelSelector = type === FormTypeEnum.modelSelector + const showTypeSwitch = isNumber || isObject || isArray + const isConstant = varInput?.type === VarKindType.constant || !varInput?.type + const showVariableSelector = isFile || varInput?.type === VarKindType.variable + + const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + filterVar: (varPayload: Var) => { + return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + }, + }) + + const targetVarType = () => { + if (isString) + return VarType.string + else if (isNumber) + return VarType.number + else if (type === FormTypeEnum.files) + return VarType.arrayFile + else if (type === FormTypeEnum.file) + return VarType.file + // else if (isSelect) + // return VarType.select + // else if (isAppSelector) + // return VarType.appSelector + // else if (isModelSelector) + // return VarType.modelSelector + // else if (isBoolean) + // return VarType.boolean + else if (isObject) + return VarType.object + else if (isArray) + return VarType.arrayObject + else + return VarType.string + } + + const getFilterVar = () => { + if (isNumber) + return (varPayload: any) => varPayload.type === VarType.number + else if (isString) + return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + else if (isFile) + return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type) + else if (isBoolean) + return (varPayload: any) => varPayload.type === VarType.boolean + else if (isObject) + return (varPayload: any) => varPayload.type === VarType.object + else if (isArray) + return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type) + return undefined + } + + const getVarKindType = () => { + if (isFile) + return VarKindType.variable + if (isSelect || isBoolean || isNumber || isArray || isObject) + return VarKindType.constant + if (isString) + return VarKindType.mixed + } + + const handleTypeChange = (newType: string) => { + if (newType === VarKindType.variable) { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.variable, + value: '', + }, + }) + } + else { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.constant, + value: defaultValue, + }, + }) + } + } + + const handleValueChange = (newValue: any) => { + onChange({ + ...value, + [variable]: { + ...varInput, + type: getVarKindType(), + value: isNumber ? Number.parseFloat(newValue) : newValue, + }, + }) + } + + const handleAppOrModelSelect = (newValue: any) => { + onChange({ + ...value, + [variable]: { + ...varInput, + ...newValue, + }, + }) + } + + const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.variable, + value: newValue || '', + }, + }) + } + + return ( + <div className={cn('gap-1', !(isShowJSONEditor && isConstant) && 'flex')}> + {showTypeSwitch && ( + <FormInputTypeSwitch value={varInput?.type || VarKindType.constant} onChange={handleTypeChange}/> + )} + {isString && ( + <MixedVariableTextInput + readOnly={readOnly} + value={varInput?.value as string || ''} + onChange={handleValueChange} + nodesOutputVars={availableVars} + availableNodes={availableNodesWithParent} + /> + )} + {isNumber && isConstant && ( + <Input + className='h-8 grow' + type='number' + value={varInput?.value || ''} + onChange={e => handleValueChange(e.target.value)} + placeholder={placeholder?.[language] || placeholder?.en_US} + /> + )} + {isBoolean && ( + <FormInputBoolean + value={varInput?.value as boolean} + onChange={handleValueChange} + /> + )} + {isSelect && ( + <SimpleSelect + wrapperClassName='h-8 grow' + disabled={readOnly} + defaultValue={varInput?.value} + items={options.filter((option: { show_on: any[] }) => { + if (option.show_on.length) + return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) + + return true + }).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))} + onSelect={item => handleValueChange(item.value as string)} + placeholder={placeholder?.[language] || placeholder?.en_US} + /> + )} + {isShowJSONEditor && isConstant && ( + <div className='mt-1 w-full'> + <CodeEditor + title='JSON' + value={varInput?.value as any} + isExpand + isInNode + language={CodeLanguage.json} + onChange={handleValueChange} + className='w-full' + placeholder={<div className='whitespace-pre'>{placeholder?.[language] || placeholder?.en_US}</div>} + /> + </div> + )} + {isAppSelector && ( + <AppSelector + disabled={readOnly} + scope={scope || 'all'} + value={varInput as any} + onSelect={handleAppOrModelSelect} + /> + )} + {isModelSelector && isConstant && ( + <ModelParameterModal + popupClassName='!w-[387px]' + isAdvancedMode + isInWorkflow + value={varInput} + setModel={handleAppOrModelSelect} + readonly={readOnly} + scope={scope} + /> + )} + {showVariableSelector && ( + <VarReferencePicker + zIndex={inPanel ? 1000 : undefined} + className='h-8 grow' + readonly={readOnly} + isShowNodeName + nodeId={nodeId} + value={varInput?.value || []} + onChange={value => handleVariableSelectorChange(value, variable)} + filterVar={getFilterVar()} + schema={schema} + valueTypePlaceHolder={targetVarType()} + currentTool={currentTool} + currentProvider={currentProvider} + /> + )} + </div> + ) +} +export default FormInputItem diff --git a/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx new file mode 100644 index 0000000000..391e204844 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx @@ -0,0 +1,47 @@ +'use client' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiEditLine, +} from '@remixicon/react' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import Tooltip from '@/app/components/base/tooltip' +import { VarType } from '@/app/components/workflow/nodes/tool/types' +import cn from '@/utils/classnames' + +type Props = { + value: VarType + onChange: (value: VarType) => void +} + +const FormInputTypeSwitch: FC<Props> = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + return ( + <div className='inline-flex h-8 shrink-0 gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5'> + <Tooltip + popupContent={value === VarType.variable ? '' : t('workflow.nodes.common.typeSwitch.variable')} + > + <div + className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.variable && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')} + onClick={() => onChange(VarType.variable)} + > + <Variable02 className='h-4 w-4' /> + </div> + </Tooltip> + <Tooltip + popupContent={value === VarType.constant ? '' : t('workflow.nodes.common.typeSwitch.input')} + > + <div + className={cn('cursor-pointer rounded-lg px-2.5 py-1.5 text-text-tertiary hover:bg-state-base-hover', value === VarType.constant && 'bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg')} + onClick={() => onChange(VarType.constant)} + > + <RiEditLine className='h-4 w-4' /> + </div> + </Tooltip> + </div> + ) +} +export default FormInputTypeSwitch diff --git a/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx new file mode 100644 index 0000000000..8117f7502f --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx @@ -0,0 +1,22 @@ +'use client' +import Tooltip from '@/app/components/base/tooltip' +import { RiAlertFill } from '@remixicon/react' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +const McpToolNotSupportTooltip: FC = () => { + const { t } = useTranslation() + return ( + <Tooltip + popupContent={ + <div className='w-[256px]'> + {t('plugin.detailPanel.toolSelector.unsupportedMCPTool')} + </div> + } + > + <RiAlertFill className='size-4 text-text-warning-secondary' /> + </Tooltip> + ) +} +export default React.memo(McpToolNotSupportTooltip) diff --git a/web/app/components/workflow/nodes/_base/components/memory-config.tsx b/web/app/components/workflow/nodes/_base/components/memory-config.tsx index 446fcfa8ae..168ad656b6 100644 --- a/web/app/components/workflow/nodes/_base/components/memory-config.tsx +++ b/web/app/components/workflow/nodes/_base/components/memory-config.tsx @@ -53,7 +53,7 @@ type Props = { const MEMORY_DEFAULT: Memory = { window: { enabled: false, size: WINDOW_SIZE_DEFAULT }, - query_prompt_template: '{{#sys.query#}}', + query_prompt_template: '{{#sys.query#}}\n\n{{#sys.files#}}', } const MemoryConfig: FC<Props> = ({ diff --git a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx index afb642955c..25d1a0aa63 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -1,10 +1,10 @@ import { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { isEqual } from 'lodash-es' import { getConnectedEdges, getOutgoers, - useEdges, - useStoreApi, + useStore, } from 'reactflow' import { useToolIcon } from '../../../../hooks' import BlockIcon from '../../../../block-icon' @@ -26,12 +26,21 @@ const NextStep = ({ const { t } = useTranslation() const data = selectedNode.data const toolIcon = useToolIcon(data) - const store = useStoreApi() const branches = useMemo(() => { return data._targetBranches || [] }, [data]) - const edges = useEdges() - const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges) + const edges = useStore(s => s.edges.map(edge => ({ + id: edge.id, + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle, + })), isEqual) + const nodes = useStore(s => s.getNodes().map(node => ({ + id: node.id, + data: node.data, + })), isEqual) + const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges) const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id) const list = useMemo(() => { diff --git a/web/app/components/workflow/nodes/_base/components/node-control.tsx b/web/app/components/workflow/nodes/_base/components/node-control.tsx index a85c41741b..5b92b7b6b4 100644 --- a/web/app/components/workflow/nodes/_base/components/node-control.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-control.tsx @@ -13,7 +13,7 @@ import { useNodesInteractions, useNodesSyncDraft, } from '../../../hooks' -import type { Node } from '../../../types' +import { type Node, NodeRunningStatus } from '../../../types' import { canRunBySingle } from '../../../utils' import PanelOperator from './panel-operator' import { @@ -31,11 +31,12 @@ const NodeControl: FC<NodeControlProps> = ({ const { handleNodeDataUpdate } = useNodeDataUpdate() const { handleNodeSelect } = useNodesInteractions() const { handleSyncWorkflowDraft } = useNodesSyncDraft() - + const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running const handleOpenChange = useCallback((newOpen: boolean) => { setOpen(newOpen) }, []) + const isChildNode = !!(data.isInIteration || data.isInLoop) return ( <div className={` @@ -49,23 +50,25 @@ const NodeControl: FC<NodeControlProps> = ({ onClick={e => e.stopPropagation()} > { - canRunBySingle(data.type) && ( + canRunBySingle(data.type, isChildNode) && ( <div className='flex h-5 w-5 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover' onClick={() => { + const nextData: Record<string, any> = { + _isSingleRun: !isSingleRunning, + } + if(isSingleRunning) + nextData._singleRunningStatus = undefined + handleNodeDataUpdate({ id, - data: { - _isSingleRun: !data._isSingleRun, - }, + data: nextData, }) handleNodeSelect(id) - if (!data._isSingleRun) - handleSyncWorkflowDraft(true) }} > { - data._isSingleRun + isSingleRunning ? <Stop className='h-3 w-3' /> : ( <Tooltip diff --git a/web/app/components/workflow/nodes/_base/components/node-position.tsx b/web/app/components/workflow/nodes/_base/components/node-position.tsx index 404648dfa6..e844726b4f 100644 --- a/web/app/components/workflow/nodes/_base/components/node-position.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-position.tsx @@ -1,30 +1,39 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { useShallow } from 'zustand/react/shallow' import { RiCrosshairLine } from '@remixicon/react' -import type { XYPosition } from 'reactflow' -import { useReactFlow, useStoreApi } from 'reactflow' +import { useReactFlow, useStore } from 'reactflow' import TooltipPlus from '@/app/components/base/tooltip' import { useNodesSyncDraft } from '@/app/components/workflow-app/hooks' type NodePositionProps = { - nodePosition: XYPosition, - nodeWidth?: number | null, - nodeHeight?: number | null, + nodeId: string } const NodePosition = ({ - nodePosition, - nodeWidth, - nodeHeight, + nodeId, }: NodePositionProps) => { const { t } = useTranslation() const reactflow = useReactFlow() - const store = useStoreApi() const { doSyncWorkflowDraft } = useNodesSyncDraft() + const { + nodePosition, + nodeWidth, + nodeHeight, + } = useStore(useShallow((s) => { + const nodes = s.getNodes() + const currentNode = nodes.find(node => node.id === nodeId)! + + return { + nodePosition: currentNode.position, + nodeWidth: currentNode.width, + nodeHeight: currentNode.height, + } + })) + const transform = useStore(s => s.transform) if (!nodePosition || !nodeWidth || !nodeHeight) return null const workflowContainer = document.getElementById('workflow-container') - const { transform } = store.getState() const zoom = transform[2] const { clientWidth, clientHeight } = workflowContainer! diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx index 28d7358ddb..b079c7b5e0 100644 --- a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx @@ -83,14 +83,16 @@ const PanelOperatorPopup = ({ const link = useNodeHelpLink(data.type) + const isChildNode = !!(data.isInIteration || data.isInLoop) + return ( <div className='w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'> { - (showChangeBlock || canRunBySingle(data.type)) && ( + (showChangeBlock || canRunBySingle(data.type, isChildNode)) && ( <> <div className='p-1'> { - canRunBySingle(data.type) && ( + canRunBySingle(data.type, isChildNode) && ( <div className={` flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 0a7ebc2a09..b28e8a1ca3 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -173,7 +173,6 @@ const Editor: FC<Props> = ({ <a className='text-text-accent' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a> </div> } - needsDelay > <div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}> <Jinja className='h-3 w-6 text-text-quaternary' /> diff --git a/web/app/components/workflow/nodes/_base/components/setting-item.tsx b/web/app/components/workflow/nodes/_base/components/setting-item.tsx index 134bf4a551..abbfaef490 100644 --- a/web/app/components/workflow/nodes/_base/components/setting-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/setting-item.tsx @@ -13,7 +13,7 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt const indicator: ComponentProps<typeof Indicator>['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined const needTooltip = ['error', 'warning'].includes(status as any) return <div className='relative flex items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1.5 py-1 text-xs font-normal'> - <div className={classNames('shrink-0 truncate text-text-tertiary system-xs-medium-uppercase', !!children && 'max-w-[100px]')}> + <div className={classNames('system-xs-medium-uppercase max-w-full shrink-0 truncate text-text-tertiary', !!children && 'max-w-[100px]')}> {label} </div> <Tooltip popupContent={tooltip} disabled={!needTooltip}> diff --git a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx index ceb38fbcfd..94b3ce7bfc 100644 --- a/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx +++ b/web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx @@ -15,7 +15,7 @@ import { pluginManifestToCardPluginProps } from '@/app/components/plugins/instal import { Badge as Badge2, BadgeState } from '@/app/components/base/badge/index' import Link from 'next/link' import { useTranslation } from 'react-i18next' -import { marketplaceUrlPrefix } from '@/config' +import { getMarketplaceUrl } from '@/utils/var' export type SwitchPluginVersionProps = { uniqueIdentifier: string @@ -82,7 +82,7 @@ export const SwitchPluginVersion: FC<SwitchPluginVersionProps> = (props) => { modalBottomLeft={ <Link className='flex items-center justify-center gap-1' - href={`${marketplaceUrlPrefix}/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`} + href={getMarketplaceUrl(`/plugins/${pluginDetail.declaration.author}/${pluginDetail.declaration.name}`)} target='_blank' > <span className='system-xs-regular text-xs text-text-accent'> diff --git a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx index 0adfa3c9fb..0d965e2d22 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx @@ -13,6 +13,8 @@ type Props = { readonly: boolean value: string onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void + onOpenChange?: (open: boolean) => void + isLoading?: boolean } const DEFAULT_SCHEMA = {} as CredentialFormSchema @@ -22,6 +24,8 @@ const ConstantField: FC<Props> = ({ readonly, value, onChange, + onOpenChange, + isLoading, }) => { const language = useLanguage() const placeholder = (schema as CredentialFormSchemaSelect).placeholder @@ -36,7 +40,7 @@ const ConstantField: FC<Props> = ({ return ( <> - {schema.type === FormTypeEnum.select && ( + {(schema.type === FormTypeEnum.select || schema.type === FormTypeEnum.dynamicSelect) && ( <SimpleSelect wrapperClassName='w-full !h-8' className='flex items-center' @@ -45,6 +49,8 @@ const ConstantField: FC<Props> = ({ items={(schema as CredentialFormSchemaSelect).options.map(option => ({ value: option.value, name: option.label[language] || option.label.en_US }))} onSelect={item => handleSelectChange(item.value)} placeholder={placeholder?.[language] || placeholder?.en_US} + onOpenChange={onOpenChange} + isLoading={isLoading} /> )} {schema.type === FormTypeEnum.textNumber && ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx index 302ed3ca75..5a84300fcd 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx @@ -74,7 +74,7 @@ const PickerPanel: FC<Props> = ({ ...props }) => { return ( - <div className={cn('w-[296px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 pb-0 shadow-lg backdrop-blur-[5px]', className)}> + <div className={cn('w-[296px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]', className)}> <PickerPanelMain {...props} /> </div> ) diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index dba93aaf97..a7c9a9d172 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -8,7 +8,7 @@ import RemoveButton from '../remove-button' import VarTypePicker from './var-type-picker' import Input from '@/app/components/base/input' import type { VarType } from '@/app/components/workflow/types' -import { checkKeys } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' import Toast from '@/app/components/base/toast' type Props = { @@ -37,6 +37,8 @@ const OutputVarList: FC<Props> = ({ const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent<HTMLInputElement>) => { const oldKey = list[index].variable + + replaceSpaceWithUnderscreInVarNameInput(e.target) const newKey = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 428c204dd3..db56feaacc 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -462,6 +462,7 @@ const formatItem = ( return { variable: `env.${env.name}`, type: env.value_type, + description: env.description, } }) as Var[] break @@ -472,7 +473,7 @@ const formatItem = ( return { variable: `conversation.${chatVar.name}`, type: chatVar.value_type, - des: chatVar.description, + description: chatVar.description, } }) as Var[] break @@ -611,6 +612,7 @@ const getIterationItemType = ({ }): VarType => { const outputVarNodeId = valueSelector[0] const isSystem = isSystemVar(valueSelector) + const isChatVar = isConversationVar(valueSelector) const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId) @@ -620,7 +622,7 @@ const getIterationItemType = ({ let arrayType: VarType = VarType.string let curr: any = targetVar.vars - if (isSystem) { + if (isSystem || isChatVar) { arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type } else { @@ -948,9 +950,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => { break } case BlockEnum.Answer: { - res = (data as AnswerNodeType).variables?.map((v) => { - return v.value_selector - }) + res = matchNotSystemVars([(data as AnswerNodeType).answer]) break } case BlockEnum.LLM: { @@ -977,6 +977,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => { res = (data as IfElseNodeType).conditions?.map((c) => { return c.variable_selector || [] }) || [] + res.push(...((data as IfElseNodeType).cases || []).flatMap(c => (c.conditions || [])).map(c => c.variable_selector || [])) break } case BlockEnum.Code: { @@ -996,6 +997,9 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => { res = [payload.query_variable_selector] const varInInstructions = matchNotSystemVars([payload.instruction || '']) res.push(...varInInstructions) + + const classes = payload.classes.map(c => c.name) + res.push(...matchNotSystemVars(classes)) break } case BlockEnum.HttpRequest: { @@ -1090,13 +1094,13 @@ export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSe break } case BlockEnum.Code: { - const targetVar = (data as CodeNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) + const targetVar = (data as CodeNodeType).variables?.find(v => Array.isArray(v.value_selector) && v.value_selector && v.value_selector.join('.') === valueSelector.join('.')) if (targetVar) res = targetVar.variable break } case BlockEnum.TemplateTransform: { - const targetVar = (data as TemplateTransformNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) + const targetVar = (data as TemplateTransformNodeType).variables?.find(v => Array.isArray(v.value_selector) && v.value_selector && v.value_selector.join('.') === valueSelector.join('.')) if (targetVar) res = targetVar.variable break diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 72e9384d5f..9eb34ac7f2 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -8,6 +8,8 @@ import VarReferencePicker from './var-reference-picker' import Input from '@/app/components/base/input' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import Toast from '@/app/components/base/toast' type Props = { nodeId: string @@ -36,24 +38,53 @@ const VarList: FC<Props> = ({ const handleVarNameChange = useCallback((index: number) => { return (e: React.ChangeEvent<HTMLInputElement>) => { - onVarNameChange?.(list[index].variable, e.target.value) + replaceSpaceWithUnderscreInVarNameInput(e.target) + + const newKey = e.target.value + const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true) + if (!isValid) { + Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + }) + return + } + + if (list.map(item => item.variable?.trim()).includes(newKey.trim())) { + Toast.notify({ + type: 'error', + message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }), + }) + return + } + + onVarNameChange?.(list[index].variable, newKey) const newList = produce(list, (draft) => { - draft[index].variable = e.target.value + draft[index].variable = newKey }) onChange(newList) } }, [list, onVarNameChange, onChange]) const handleVarReferenceChange = useCallback((index: number) => { - return (value: ValueSelector | string, varKindType: VarKindType) => { + return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { const newList = produce(list, (draft) => { if (!isSupportConstantValue || varKindType === VarKindType.variable) { draft[index].value_selector = value as ValueSelector + draft[index].value_type = varInfo?.type if (isSupportConstantValue) draft[index].variable_type = VarKindType.variable - if (!draft[index].variable) - draft[index].variable = value[value.length - 1] + if (!draft[index].variable) { + const variables = draft.map(v => v.variable) + let newVarName = value[value.length - 1] + let count = 1 + while (variables.includes(newVarName)) { + newVarName = `${value[value.length - 1]}_${count}` + count++ + } + draft[index].variable = newVarName + } } else { draft[index].variable_type = VarKindType.constant diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index e9825cd44a..e6f3ce1fa1 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -6,6 +6,7 @@ import { RiArrowDownSLine, RiCloseLine, RiErrorWarningFill, + RiLoader4Line, RiMoreLine, } from '@remixicon/react' import produce from 'immer' @@ -16,8 +17,9 @@ import VarReferencePopup from './var-reference-popup' import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils' import ConstantField from './constant-field' import cn from '@/utils/classnames' -import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' +import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { BlockEnum } from '@/app/components/workflow/types' import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { Line3 } from '@/app/components/base/icons/src/public/common' @@ -40,6 +42,8 @@ import Tooltip from '@/app/components/base/tooltip' import { isExceptionVariable } from '@/app/components/workflow/utils' import VarFullPathPanel from './var-full-path-panel' import { noop } from 'lodash-es' +import { useFetchDynamicOptions } from '@/service/use-plugins' +import type { Tool } from '@/app/components/tools/types' const TRIGGER_DEFAULT_WIDTH = 227 @@ -68,6 +72,8 @@ type Props = { minWidth?: number popupFor?: 'assigned' | 'toAssigned' zIndex?: number + currentTool?: Tool + currentProvider?: ToolWithProvider } const DEFAULT_VALUE_SELECTOR: Props['value'] = [] @@ -97,6 +103,8 @@ const VarReferencePicker: FC<Props> = ({ minWidth, popupFor, zIndex, + currentTool, + currentProvider, }) => { const { t } = useTranslation() const store = useStoreApi() @@ -176,9 +184,11 @@ const VarReferencePicker: FC<Props> = ({ return startNode?.data const node = getNodeInfoById(availableNodes, outputVarNodeId)?.data - return { - ...node, - id: outputVarNodeId, + if (node) { + return { + ...node, + id: outputVarNodeId, + } } }, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode]) @@ -316,6 +326,42 @@ const VarReferencePicker: FC<Props> = ({ return null }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) + + const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null) + const [isLoading, setIsLoading] = useState(false) + const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions( + currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '', + 'tool', + ) + const handleFetchDynamicOptions = async () => { + if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider) + return + setIsLoading(true) + try { + const data = await fetchDynamicOptions() + setDynamicOptions(data?.options || []) + } + finally { + setIsLoading(false) + } + } + useEffect(() => { + handleFetchDynamicOptions() + }, [currentTool, currentProvider, schema]) + + const schemaWithDynamicSelect = useMemo(() => { + if (schema?.type !== FormTypeEnum.dynamicSelect) + return schema + // rewrite schema.options with dynamicOptions + if (dynamicOptions) { + return { + ...schema, + options: dynamicOptions, + } + } + return schema + }, [dynamicOptions]) + return ( <div className={cn(className, !readonly && 'cursor-pointer')}> <PortalToFollowElem @@ -366,8 +412,9 @@ const VarReferencePicker: FC<Props> = ({ <ConstantField value={value as string} onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)} - schema={schema as CredentialFormSchema} + schema={schemaWithDynamicSelect as CredentialFormSchema} readonly={readonly} + isLoading={isLoading} /> ) : ( @@ -412,6 +459,7 @@ const VarReferencePicker: FC<Props> = ({ )} <div className='flex items-center text-text-accent'> {!hasValue && <Variable02 className='h-3.5 w-3.5' />} + {isLoading && <RiLoader4Line className='h-3.5 w-3.5 animate-spin text-text-secondary' />} {isEnv && <Env className='h-3.5 w-3.5 text-util-colors-violet-violet-600' />} {isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />} <div className={cn('ml-0.5 truncate text-xs font-medium', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{ @@ -424,7 +472,16 @@ const VarReferencePicker: FC<Props> = ({ {!isValidVar && <RiErrorWarningFill className='ml-0.5 h-3 w-3 text-text-destructive' />} </> ) - : <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</div>} + : <div className={`overflow-hidden ${readonly ? 'text-components-input-text-disabled' : 'text-components-input-text-placeholder'} system-sm-regular text-ellipsis`}> + {isLoading ? ( + <div className='flex items-center'> + <RiLoader4Line className='mr-1 h-3.5 w-3.5 animate-spin text-text-secondary' /> + <span>{placeholder ?? t('workflow.common.setVarValuePlaceholder')}</span> + </div> + ) : ( + placeholder ?? t('workflow.common.setVarValuePlaceholder') + )} + </div>} </div> </Tooltip> </div> @@ -471,6 +528,7 @@ const VarReferencePicker: FC<Props> = ({ onChange={handleVarReferenceChange} itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)} isSupportFileVar={isSupportFileVar} + zIndex={zIndex} /> )} </PortalToFollowElemContent> diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index e35977ae31..3746a85441 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -2,12 +2,10 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import VarReferenceVars from './var-reference-vars' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import ListEmpty from '@/app/components/base/list-empty' -import { LanguagesSupported } from '@/i18n/language' -import I18n from '@/context/i18n' +import { useDocLink } from '@/context/i18n' type Props = { vars: NodeOutPutVar[] @@ -15,6 +13,7 @@ type Props = { onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number isSupportFileVar?: boolean + zIndex?: number } const VarReferencePopup: FC<Props> = ({ vars, @@ -22,9 +21,10 @@ const VarReferencePopup: FC<Props> = ({ onChange, itemWidth, isSupportFileVar = true, + zIndex, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() // max-h-[300px] overflow-y-auto todo: use portal to handle long list return ( <div className='space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg' style={{ @@ -47,7 +47,12 @@ const VarReferencePopup: FC<Props> = ({ {t('workflow.variableReference.assignedVarsDescription')} <a target='_blank' rel='noopener noreferrer' className='text-text-accent-secondary' - href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/guides/workflow/variables#conversation-variables' : `https://docs.dify.ai/${locale.toLowerCase()}/guides/workflow/variables#hui-hua-bian-liang`}>{t('workflow.variableReference.conversationVars')}</a> + href={docLink('/guides/workflow/variables#conversation-variables', { + 'zh-Hans': '/guides/workflow/variables#会话变量', + 'ja-JP': '/guides/workflow/variables#会話変数', + })}> + {t('workflow.variableReference.conversationVars')} + </a> </div>} /> )) @@ -57,6 +62,7 @@ const VarReferencePopup: FC<Props> = ({ onChange={onChange} itemWidth={itemWidth} isSupportFileVar={isSupportFileVar} + zIndex={zIndex} /> } </div > diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 023916ec5b..303840d8e7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -46,6 +46,7 @@ type ItemProps = { isSupportFileVar?: boolean isException?: boolean isLoopVar?: boolean + zIndex?: number } const objVarTypes = [VarType.object, VarType.file] @@ -60,6 +61,7 @@ const Item: FC<ItemProps> = ({ isSupportFileVar, isException, isLoopVar, + zIndex, }) => { const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties const isFile = itemData.type === VarType.file && !isStructureOutput @@ -141,7 +143,7 @@ const Item: FC<ItemProps> = ({ ref={itemRef} className={cn( (isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]', - isHovering && ((isObj || isStructureOutput) ? 'bg-primary-50' : 'bg-state-base-hover'), + isHovering && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'), 'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3') } onClick={handleChosen} @@ -171,7 +173,7 @@ const Item: FC<ItemProps> = ({ </div > </PortalToFollowElemTrigger > <PortalToFollowElemContent style={{ - zIndex: 100, + zIndex: zIndex || 100, }}> {(isStructureOutput || isObj) && ( <PickerStructurePanel @@ -260,6 +262,8 @@ type Props = { maxHeightClass?: string onClose?: () => void onBlur?: () => void + zIndex?: number + autoFocus?: boolean } const VarReferenceVars: FC<Props> = ({ hideSearch, @@ -271,6 +275,8 @@ const VarReferenceVars: FC<Props> = ({ maxHeightClass, onClose, onBlur, + zIndex, + autoFocus = true, }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') @@ -323,7 +329,7 @@ const VarReferenceVars: FC<Props> = ({ onKeyDown={handleKeyDown} onClear={() => setSearchText('')} onBlur={onBlur} - autoFocus + autoFocus={autoFocus} /> </div> <div className='relative left-[-4px] h-[0.5px] bg-black/5' style={{ @@ -355,6 +361,7 @@ const VarReferenceVars: FC<Props> = ({ isSupportFileVar={isSupportFileVar} isException={v.isException} isLoopVar={item.isLoop} + zIndex={zIndex} /> ))} </div>)) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx new file mode 100644 index 0000000000..b2a09e2f10 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -0,0 +1,434 @@ +import type { + FC, + ReactNode, +} from 'react' +import { + cloneElement, + memo, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { + RiCloseLine, + RiPlayLargeLine, +} from '@remixicon/react' +import { useShallow } from 'zustand/react/shallow' +import { useTranslation } from 'react-i18next' +import NextStep from '../next-step' +import PanelOperator from '../panel-operator' +import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position' +import HelpLink from '../help-link' +import { + DescriptionInput, + TitleInput, +} from '../title-description-input' +import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel' +import RetryOnPanel from '../retry/retry-on-panel' +import { useResizePanel } from '../../hooks/use-resize-panel' +import cn from '@/utils/classnames' +import BlockIcon from '@/app/components/workflow/block-icon' +import Split from '@/app/components/workflow/nodes/_base/components/split' +import { + WorkflowHistoryEvent, + useAvailableBlocks, + useNodeDataUpdate, + useNodesInteractions, + useNodesReadOnly, + useToolIcon, + useWorkflowHistory, +} from '@/app/components/workflow/hooks' +import { + canRunBySingle, + hasErrorHandleNode, + hasRetryNode, +} from '@/app/components/workflow/utils' +import Tooltip from '@/app/components/base/tooltip' +import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types' +import { useStore as useAppStore } from '@/app/components/app/store' +import { useStore } from '@/app/components/workflow/store' +import Tab, { TabType } from './tab' +import LastRun from './last-run' +import useLastRun from './last-run/use-last-run' +import BeforeRunForm from '../before-run-form' +import { debounce } from 'lodash-es' +import { NODES_EXTRA_DATA } from '@/app/components/workflow/constants' +import { useLogs } from '@/app/components/workflow/run/hooks' +import PanelWrap from '../before-run-form/panel-wrap' +import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' +import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' + +type BasePanelProps = { + children: ReactNode + id: Node['id'] + data: Node['data'] +} + +const BasePanel: FC<BasePanelProps> = ({ + id, + data, + children, +}) => { + const { t } = useTranslation() + const { showMessageLogModal } = useAppStore(useShallow(state => ({ + showMessageLogModal: state.showMessageLogModal, + }))) + const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running + + const showSingleRunPanel = useStore(s => s.showSingleRunPanel) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const otherPanelWidth = useStore(s => s.otherPanelWidth) + const setNodePanelWidth = useStore(s => s.setNodePanelWidth) + + const reservedCanvasWidth = 400 // Reserve the minimum visible width for the canvas + + const maxNodePanelWidth = useMemo(() => { + if (!workflowCanvasWidth) + return 720 + + const available = workflowCanvasWidth - (otherPanelWidth || 0) - reservedCanvasWidth + return Math.max(available, 400) + }, [workflowCanvasWidth, otherPanelWidth]) + + const updateNodePanelWidth = useCallback((width: number) => { + // Ensure the width is within the min and max range + const newValue = Math.max(400, Math.min(width, maxNodePanelWidth)) + localStorage.setItem('workflow-node-panel-width', `${newValue}`) + setNodePanelWidth(newValue) + }, [maxNodePanelWidth, setNodePanelWidth]) + + const handleResize = useCallback((width: number) => { + updateNodePanelWidth(width) + }, [updateNodePanelWidth]) + + const { + triggerRef, + containerRef, + } = useResizePanel({ + direction: 'horizontal', + triggerDirection: 'left', + minWidth: 400, + maxWidth: maxNodePanelWidth, + onResize: debounce(handleResize), + }) + + const debounceUpdate = debounce(updateNodePanelWidth) + useEffect(() => { + if (!workflowCanvasWidth) + return + + // If the total width of the three exceeds the canvas, shrink the node panel to the available range (at least 400px) + const total = nodePanelWidth + otherPanelWidth + reservedCanvasWidth + if (total > workflowCanvasWidth) { + const target = Math.max(workflowCanvasWidth - otherPanelWidth - reservedCanvasWidth, 400) + debounceUpdate(target) + } + }, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth]) + + const { handleNodeSelect } = useNodesInteractions() + const { nodesReadOnly } = useNodesReadOnly() + const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) + const toolIcon = useToolIcon(data) + + const { saveStateToHistory } = useWorkflowHistory() + + const { + handleNodeDataUpdate, + handleNodeDataUpdateWithSyncDraft, + } = useNodeDataUpdate() + + const handleTitleBlur = useCallback((title: string) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) + saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) + const handleDescriptionChange = useCallback((desc: string) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) + saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) + }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) + + const isChildNode = !!(data.isInIteration || data.isInLoop) + const isSupportSingleRun = canRunBySingle(data.type, isChildNode) + const appDetail = useAppStore(state => state.appDetail) + + const hasClickRunning = useRef(false) + const [isPaused, setIsPaused] = useState(false) + + useEffect(() => { + if(data._singleRunningStatus === NodeRunningStatus.Running) { + hasClickRunning.current = true + setIsPaused(false) + } + else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) { + setIsPaused(true) + hasClickRunning.current = false + } + }, [data]) + + const updateNodeRunningStatus = useCallback((status: NodeRunningStatus) => { + handleNodeDataUpdate({ + id, + data: { + ...data, + _singleRunningStatus: status, + }, + }) + }, [handleNodeDataUpdate, id, data]) + + useEffect(() => { + // console.log(`id changed: ${id}, hasClickRunning: ${hasClickRunning.current}`) + hasClickRunning.current = false + }, [id]) + + const { + isShowSingleRun, + hideSingleRun, + runningStatus, + handleStop, + runInputData, + runInputDataRef, + runResult, + getInputVars, + toVarInputs, + tabType, + isRunAfterSingleRun, + setTabType, + singleRunParams, + nodeInfo, + setRunInputData, + handleSingleRun, + handleRunWithParams, + getExistVarValuesInForms, + getFilteredExistVarForms, + } = useLastRun<typeof data>({ + id, + data, + defaultRunInputData: NODES_EXTRA_DATA[data.type]?.defaultRunInputData || {}, + isPaused, + }) + + useEffect(() => { + setIsPaused(false) + }, [tabType]) + + const logParams = useLogs() + const passedLogParams = (() => { + if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type)) + return logParams + + return {} + })() + + if(logParams.showSpecialResultPanel) { + return ( + <div className={cn( + 'relative mr-1 h-full', + )}> + <div + ref={containerRef} + className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} + style={{ + width: `${nodePanelWidth}px`, + }} + > + <PanelWrap + nodeName={data.title} + onHide={hideSingleRun} + > + <div className='h-0 grow overflow-y-auto pb-4'> + <SpecialResultPanel {...passedLogParams} /> + </div> + </PanelWrap> + </div> + </div> + ) + } + + if (isShowSingleRun) { + return ( + <div className={cn( + 'relative mr-1 h-full', + )}> + <div + ref={containerRef} + className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} + style={{ + width: `${nodePanelWidth}px`, + }} + > + <BeforeRunForm + nodeName={data.title} + nodeType={data.type} + onHide={hideSingleRun} + onRun={handleRunWithParams} + {...singleRunParams!} + {...passedLogParams} + existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)} + filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)} + /> + </div> + </div> + ) + } + + return ( + <div className={cn( + 'relative mr-1 h-full', + showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all', + )}> + <div + ref={triggerRef} + className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'> + <div className='h-10 w-0.5 rounded-sm bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid'></div> + </div> + <div + ref={containerRef} + className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} + style={{ + width: `${nodePanelWidth}px`, + }} + > + <div className='sticky top-0 z-10 shrink-0 border-b-[0.5px] border-divider-regular bg-components-panel-bg'> + <div className='flex items-center px-4 pb-1 pt-4'> + <BlockIcon + className='mr-1 shrink-0' + type={data.type} + toolIcon={toolIcon} + size='md' + /> + <TitleInput + value={data.title || ''} + onBlur={handleTitleBlur} + /> + <div className='flex shrink-0 items-center text-text-tertiary'> + { + isSupportSingleRun && !nodesReadOnly && ( + <Tooltip + popupContent={t('workflow.panel.runThisStep')} + popupClassName='mr-1' + disabled={isSingleRunning} + > + <div + className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover' + onClick={() => { + if(isSingleRunning) { + handleNodeDataUpdate({ + id, + data: { + _isSingleRun: false, + _singleRunningStatus: undefined, + }, + }) + } + else { + handleSingleRun() + } + }} + > + { + isSingleRunning ? <Stop className='h-4 w-4 text-text-tertiary' /> + : <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' /> + } + </div> + </Tooltip> + ) + } + <NodePosition nodeId={id}></NodePosition> + <HelpLink nodeType={data.type} /> + <PanelOperator id={id} data={data} showHelpLink={false} /> + <div className='mx-3 h-3.5 w-[1px] bg-divider-regular' /> + <div + className='flex h-6 w-6 cursor-pointer items-center justify-center' + onClick={() => handleNodeSelect(id, true)} + > + <RiCloseLine className='h-4 w-4 text-text-tertiary' /> + </div> + </div> + </div> + <div className='p-2'> + <DescriptionInput + value={data.desc || ''} + onChange={handleDescriptionChange} + /> + </div> + <div className='pl-4'> + <Tab + value={tabType} + onChange={setTabType} + /> + </div> + <Split /> + </div> + + {tabType === TabType.settings && ( + <> + <div> + {cloneElement(children as any, { + id, + data, + panelProps: { + getInputVars, + toVarInputs, + runInputData, + setRunInputData, + runResult, + runInputDataRef, + }, + })} + </div> + <Split /> + { + hasRetryNode(data.type) && ( + <RetryOnPanel + id={id} + data={data} + /> + ) + } + { + hasErrorHandleNode(data.type) && ( + <ErrorHandleOnPanel + id={id} + data={data} + /> + ) + } + { + !!availableNextBlocks.length && ( + <div className='border-t-[0.5px] border-divider-regular p-4'> + <div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'> + {t('workflow.panel.nextStep').toLocaleUpperCase()} + </div> + <div className='system-xs-regular mb-2 text-text-tertiary'> + {t('workflow.panel.addNextStep')} + </div> + <NextStep selectedNode={{ id, data } as Node} /> + </div> + ) + } + </> + )} + + {tabType === TabType.lastRun && ( + <LastRun + appId={appDetail?.id || ''} + nodeId={id} + canSingleRun={isSupportSingleRun} + runningStatus={runningStatus} + isRunAfterSingleRun={isRunAfterSingleRun} + updateNodeRunningStatus={updateNodeRunningStatus} + onSingleRunClicked={handleSingleRun} + nodeInfo={nodeInfo} + singleRunResult={runResult!} + isPaused={isPaused} + {...passedLogParams} + /> + )} + </div> + </div> + ) +} + +export default memo(BasePanel) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx new file mode 100644 index 0000000000..a029987818 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx @@ -0,0 +1,126 @@ +'use client' +import type { ResultPanelProps } from '@/app/components/workflow/run/result-panel' +import ResultPanel from '@/app/components/workflow/run/result-panel' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import type { FC } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import NoData from './no-data' +import { useLastRun } from '@/service/use-workflow' +import { RiLoader2Line } from '@remixicon/react' +import type { NodeTracing } from '@/types/workflow' + +type Props = { + appId: string + nodeId: string + canSingleRun: boolean + isRunAfterSingleRun: boolean + updateNodeRunningStatus: (status: NodeRunningStatus) => void + nodeInfo?: NodeTracing + runningStatus?: NodeRunningStatus + onSingleRunClicked: () => void + singleRunResult?: NodeTracing + isPaused?: boolean +} & Partial<ResultPanelProps> + +const LastRun: FC<Props> = ({ + appId, + nodeId, + canSingleRun, + isRunAfterSingleRun, + updateNodeRunningStatus, + nodeInfo, + runningStatus: oneStepRunRunningStatus, + onSingleRunClicked, + singleRunResult, + isPaused, + ...otherResultPanelProps +}) => { + const isOneStepRunSucceed = oneStepRunRunningStatus === NodeRunningStatus.Succeeded + const isOneStepRunFailed = oneStepRunRunningStatus === NodeRunningStatus.Failed + // hide page and return to page would lost the oneStepRunRunningStatus + const [hidePageOneStepFinishedStatus, setHidePageOneStepFinishedStatus] = React.useState<NodeRunningStatus | null>(null) + const [pageHasHide, setPageHasHide] = useState(false) + const [pageShowed, setPageShowed] = useState(false) + + const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!) + const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished) + const { data: lastRunResult, isFetching, error } = useLastRun(appId, nodeId, canRunLastRun) + const isRunning = useMemo(() => { + if(isPaused) + return false + + if(!isRunAfterSingleRun) + return isFetching + return [NodeRunningStatus.Running, NodeRunningStatus.NotStart].includes(oneStepRunRunningStatus!) + }, [isFetching, isPaused, isRunAfterSingleRun, oneStepRunRunningStatus]) + + const noLastRun = (error as any)?.status === 404 + const runResult = (canRunLastRun ? lastRunResult : singleRunResult) || lastRunResult || {} + + const resetHidePageStatus = useCallback(() => { + setPageHasHide(false) + setPageShowed(false) + setHidePageOneStepFinishedStatus(null) + }, []) + useEffect(() => { + if (pageShowed && hidePageOneStepFinishedStatus && (!oneStepRunRunningStatus || oneStepRunRunningStatus === NodeRunningStatus.NotStart)) { + updateNodeRunningStatus(hidePageOneStepFinishedStatus) + resetHidePageStatus() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus]) + + useEffect(() => { + if([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(oneStepRunRunningStatus!)) + setHidePageOneStepFinishedStatus(oneStepRunRunningStatus!) + }, [oneStepRunRunningStatus]) + + useEffect(() => { + resetHidePageStatus() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodeId]) + + const handlePageVisibilityChange = useCallback(() => { + if (document.visibilityState === 'hidden') + setPageHasHide(true) + else + setPageShowed(true) + }, []) + useEffect(() => { + document.addEventListener('visibilitychange', handlePageVisibilityChange) + + return () => { + document.removeEventListener('visibilitychange', handlePageVisibilityChange) + } + }, [handlePageVisibilityChange]) + + if (isFetching && !isRunAfterSingleRun) { + return ( + <div className='flex h-0 grow flex-col items-center justify-center'> + <RiLoader2Line className='size-4 animate-spin text-text-tertiary' /> + </div>) + } + + if (isRunning) + return <ResultPanel status='running' showSteps={false} /> + + if (!isPaused && (noLastRun || !runResult)) { + return ( + <NoData canSingleRun={canSingleRun} onSingleRun={onSingleRunClicked} /> + ) + } + return ( + <div> + <ResultPanel + {...runResult as any} + {...otherResultPanelProps} + status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)} + total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens} + created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by} + nodeInfo={nodeInfo} + showSteps={false} + /> + </div> + ) +} +export default React.memo(LastRun) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx new file mode 100644 index 0000000000..ad0058efae --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx @@ -0,0 +1,36 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time' +import Button from '@/app/components/base/button' +import { RiPlayLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +type Props = { + canSingleRun: boolean + onSingleRun: () => void +} + +const NoData: FC<Props> = ({ + canSingleRun, + onSingleRun, +}) => { + const { t } = useTranslation() + return ( + <div className='flex h-0 grow flex-col items-center justify-center'> + <ClockPlay className='h-8 w-8 text-text-quaternary' /> + <div className='system-xs-regular my-2 text-text-tertiary'>{t('workflow.debug.noData.description')}</div> + {canSingleRun && ( + <Button + className='flex' + size='small' + onClick={onSingleRun} + > + <RiPlayLine className='mr-1 h-3.5 w-3.5' /> + <div>{t('workflow.debug.noData.runThisNode')}</div> + </Button> + )} + </div> + ) +} +export default React.memo(NoData) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts new file mode 100644 index 0000000000..014707cdfb --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -0,0 +1,330 @@ +import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import type { Params as OneStepRunParams } from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import { useCallback, useEffect, useState } from 'react' +import { TabType } from '../tab' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import useStartSingleRunFormParams from '@/app/components/workflow/nodes/start/use-single-run-form-params' +import useLLMSingleRunFormParams from '@/app/components/workflow/nodes/llm/use-single-run-form-params' +import useKnowledgeRetrievalSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params' +import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params' +import useTemplateTransformSingleRunFormParams from '@/app/components/workflow/nodes/template-transform/use-single-run-form-params' +import useQuestionClassifierSingleRunFormParams from '@/app/components/workflow/nodes/question-classifier/use-single-run-form-params' +import useParameterExtractorSingleRunFormParams from '@/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params' +import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params' +import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/use-single-run-form-params' +import useIterationSingleRunFormParams from '@/app/components/workflow/nodes/iteration/use-single-run-form-params' +import useAgentSingleRunFormParams from '@/app/components/workflow/nodes/agent/use-single-run-form-params' +import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params' +import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use-single-run-form-params' +import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params' +import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params' +import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params' + +import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more' +import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' + +// import +import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' +import { BlockEnum } from '@/app/components/workflow/types' +import { + useNodesSyncDraft, +} from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' +import { useInvalidLastRun } from '@/service/use-workflow' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' + +const singleRunFormParamsHooks: Record<BlockEnum, any> = { + [BlockEnum.LLM]: useLLMSingleRunFormParams, + [BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams, + [BlockEnum.Code]: useCodeSingleRunFormParams, + [BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams, + [BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams, + [BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams, + [BlockEnum.Tool]: useToolSingleRunFormParams, + [BlockEnum.ParameterExtractor]: useParameterExtractorSingleRunFormParams, + [BlockEnum.Iteration]: useIterationSingleRunFormParams, + [BlockEnum.Agent]: useAgentSingleRunFormParams, + [BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams, + [BlockEnum.Loop]: useLoopSingleRunFormParams, + [BlockEnum.Start]: useStartSingleRunFormParams, + [BlockEnum.IfElse]: useIfElseSingleRunFormParams, + [BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams, + [BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams, + [BlockEnum.VariableAssigner]: undefined, + [BlockEnum.End]: undefined, + [BlockEnum.Answer]: undefined, + [BlockEnum.ListFilter]: undefined, + [BlockEnum.IterationStart]: undefined, + [BlockEnum.LoopStart]: undefined, + [BlockEnum.LoopEnd]: undefined, +} + +const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => { + return (params: any) => { + return singleRunFormParamsHooks[nodeType]?.(params) || {} + } +} + +const getDataForCheckMoreHooks: Record<BlockEnum, any> = { + [BlockEnum.Tool]: useToolGetDataForCheckMore, + [BlockEnum.LLM]: undefined, + [BlockEnum.KnowledgeRetrieval]: undefined, + [BlockEnum.Code]: undefined, + [BlockEnum.TemplateTransform]: undefined, + [BlockEnum.QuestionClassifier]: undefined, + [BlockEnum.HttpRequest]: undefined, + [BlockEnum.ParameterExtractor]: undefined, + [BlockEnum.Iteration]: undefined, + [BlockEnum.Agent]: undefined, + [BlockEnum.DocExtractor]: undefined, + [BlockEnum.Loop]: undefined, + [BlockEnum.Start]: undefined, + [BlockEnum.IfElse]: undefined, + [BlockEnum.VariableAggregator]: undefined, + [BlockEnum.End]: undefined, + [BlockEnum.Answer]: undefined, + [BlockEnum.VariableAssigner]: undefined, + [BlockEnum.ListFilter]: undefined, + [BlockEnum.IterationStart]: undefined, + [BlockEnum.Assigner]: undefined, + [BlockEnum.LoopStart]: undefined, + [BlockEnum.LoopEnd]: undefined, +} + +const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => { + return (id: string, payload: CommonNodeType<T>) => { + return getDataForCheckMoreHooks[nodeType]?.({ id, payload }) || { + getData: () => { + return {} + }, + } + } +} + +type Params<T> = Omit<OneStepRunParams<T>, 'isRunAfterSingleRun'> +const useLastRun = <T>({ + ...oneStepRunParams +}: Params<T>) => { + const { conversationVars, systemVars, hasSetInspectVar } = useInspectVarsCrud() + const blockType = oneStepRunParams.data.type + const isStartNode = blockType === BlockEnum.Start + const isIterationNode = blockType === BlockEnum.Iteration + const isLoopNode = blockType === BlockEnum.Loop + const isAggregatorNode = blockType === BlockEnum.VariableAggregator + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { + getData: getDataForCheckMore, + } = useGetDataForCheckMoreHooks<T>(blockType)(oneStepRunParams.id, oneStepRunParams.data) + const [isRunAfterSingleRun, setIsRunAfterSingleRun] = useState(false) + + const { + id, + data, + } = oneStepRunParams + const oneStepRunRes = useOneStepRun({ + ...oneStepRunParams, + iteratorInputKey: blockType === BlockEnum.Iteration ? `${id}.input_selector` : '', + moreDataForCheckValid: getDataForCheckMore(), + isRunAfterSingleRun, + }) + + const { + appId, + hideSingleRun, + handleRun: doCallRunApi, + getInputVars, + toVarInputs, + varSelectorsToVarInputs, + runInputData, + runInputDataRef, + setRunInputData, + showSingleRun, + runResult, + iterationRunResult, + loopRunResult, + setNodeRunning, + checkValid, + } = oneStepRunRes + + const { + nodeInfo, + ...singleRunParams + } = useSingleRunFormParamsHooks(blockType)({ + id, + payload: data, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, + toVarInputs, + varSelectorsToVarInputs, + runResult, + iterationRunResult, + loopRunResult, + }) + + const toSubmitData = useCallback((data: Record<string, any>) => { + if(!isIterationNode && !isLoopNode) + return data + + const allVarObject = singleRunParams?.allVarObject || {} + const formattedData: Record<string, any> = {} + Object.keys(allVarObject).forEach((key) => { + const [varSectorStr, nodeId] = key.split(DELIMITER) + formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr] + }) + if(isIterationNode) { + const iteratorInputKey = `${id}.input_selector` + formattedData[iteratorInputKey] = data[iteratorInputKey] + } + return formattedData + }, [isIterationNode, isLoopNode, singleRunParams?.allVarObject, id]) + + const callRunApi = (data: Record<string, any>, cb?: () => void) => { + handleSyncWorkflowDraft(true, true, { + onSuccess() { + doCallRunApi(toSubmitData(data)) + cb?.() + }, + }) + } + const workflowStore = useWorkflowStore() + const { setInitShowLastRunTab } = workflowStore.getState() + const initShowLastRunTab = useStore(s => s.initShowLastRunTab) + const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings) + useEffect(() => { + if(initShowLastRunTab) + setTabType(TabType.lastRun) + + setInitShowLastRunTab(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initShowLastRunTab]) + const invalidLastRun = useInvalidLastRun(appId!, id) + + const handleRunWithParams = async (data: Record<string, any>) => { + const { isValid } = checkValid() + if(!isValid) + return + setNodeRunning() + setIsRunAfterSingleRun(true) + setTabType(TabType.lastRun) + callRunApi(data, () => { + invalidLastRun() + }) + hideSingleRun() + } + + const handleTabClicked = useCallback((type: TabType) => { + setIsRunAfterSingleRun(false) + setTabType(type) + }, []) + + const getExistVarValuesInForms = (forms: FormProps[]) => { + if (!forms || forms.length === 0) + return [] + + const valuesArr = forms.map((form) => { + const values: Record<string, boolean> = {} + form.inputs.forEach(({ variable, getVarValueFromDependent }) => { + const isGetValueFromDependent = getVarValueFromDependent || !variable.includes('.') + if(isGetValueFromDependent && !singleRunParams?.getDependentVar) + return + + const selector = isGetValueFromDependent ? (singleRunParams?.getDependentVar(variable) || []) : variable.slice(1, -1).split('.') + if(!selector || selector.length === 0) + return + const [nodeId, varName] = selector.slice(0, 2) + if(!isStartNode && nodeId === id) { // inner vars like loop vars + values[variable] = true + return + } + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + if (inspectVarValue) + values[variable] = true + }) + return values + }) + return valuesArr + } + + const isAllVarsHasValue = (vars?: ValueSelector[]) => { + if(!vars || vars.length === 0) + return true + return vars.every((varItem) => { + const [nodeId, varName] = varItem.slice(0, 2) + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + return inspectVarValue + }) + } + + const isSomeVarsHasValue = (vars?: ValueSelector[]) => { + if(!vars || vars.length === 0) + return true + return vars.some((varItem) => { + const [nodeId, varName] = varItem.slice(0, 2) + const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var + return inspectVarValue + }) + } + const getFilteredExistVarForms = (forms: FormProps[]) => { + if (!forms || forms.length === 0) + return [] + + const existVarValuesInForms = getExistVarValuesInForms(forms) + + const res = forms.map((form, i) => { + const existVarValuesInForm = existVarValuesInForms[i] + const newForm = { ...form } + const inputs = form.inputs.filter((input) => { + return !(input.variable in existVarValuesInForm) + }) + newForm.inputs = inputs + return newForm + }).filter(form => form.inputs.length > 0) + return res + } + + const checkAggregatorVarsSet = (vars: ValueSelector[][]) => { + if(!vars || vars.length === 0) + return true + // in each group, at last one set is ok + return vars.every((varItem) => { + return isSomeVarsHasValue(varItem) + }) + } + + const handleSingleRun = () => { + const { isValid } = checkValid() + if(!isValid) + return + const vars = singleRunParams?.getDependentVars?.() + // no need to input params + if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) { + callRunApi({}, async () => { + setIsRunAfterSingleRun(true) + setNodeRunning() + invalidLastRun() + setTabType(TabType.lastRun) + }) + } + else { + showSingleRun() + } + } + + return { + ...oneStepRunRes, + tabType, + isRunAfterSingleRun, + setTabType: handleTabClicked, + singleRunParams, + nodeInfo, + setRunInputData, + handleSingleRun, + handleRunWithParams, + getExistVarValuesInForms, + getFilteredExistVarForms, + } +} + +export default useLastRun diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx new file mode 100644 index 0000000000..09d7ed266d --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx @@ -0,0 +1,34 @@ +'use client' +import TabHeader from '@/app/components/base/tab-header' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +export enum TabType { + settings = 'settings', + lastRun = 'lastRun', +} + +type Props = { + value: TabType, + onChange: (value: TabType) => void +} + +const Tab: FC<Props> = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + return ( + <TabHeader + items={[ + { id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() }, + { id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() }, + ]} + itemClassName='ml-0' + value={value} + onChange={onChange as any} + /> + ) +} +export default React.memo(Tab) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts index daad6ffcc0..2141abe7ad 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts @@ -1,14 +1,12 @@ import { useMemo } from 'react' -import { useGetLanguage } from '@/context/i18n' +import { useDocLink, useGetLanguage } from '@/context/i18n' import { BlockEnum } from '@/app/components/workflow/types' export const useNodeHelpLink = (nodeType: BlockEnum) => { const language = useGetLanguage() + const docLink = useDocLink() const prefixLink = useMemo(() => { - if (language === 'zh_Hans') - return 'https://docs.dify.ai/zh-hans/guides/workflow/node/' - - return 'https://docs.dify.ai/en/guides/workflow/node/' + return docLink('/guides/workflow/node/') }, [language]) const linkMap = useMemo(() => { if (language === 'zh_Hans') { diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index f23af5812c..769804a20b 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -13,7 +13,7 @@ import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/a import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types' import { useStore as useAppStore } from '@/app/components/app/store' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import { getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow' +import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow' import Toast from '@/app/components/base/toast' import LLMDefault from '@/app/components/workflow/nodes/llm/default' import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default' @@ -32,7 +32,7 @@ import LoopDefault from '@/app/components/workflow/nodes/loop/default' import { ssePost } from '@/service/base' import { noop } from 'lodash-es' import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants' -import type { NodeTracing } from '@/types/workflow' +import type { NodeRunResult, NodeTracing } from '@/types/workflow' const { checkValid: checkLLMValid } = LLMDefault const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault const { checkValid: checkIfElseValid } = IfElseDefault @@ -47,7 +47,11 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault const { checkValid: checkIterationValid } = IterationDefault const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault const { checkValid: checkLoopValid } = LoopDefault - +import { + useStoreApi, +} from 'reactflow' +import { useInvalidLastRun } from '@/service/use-workflow' +import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' // eslint-disable-next-line ts/no-unsafe-function-type const checkValidFns: Record<BlockEnum, Function> = { [BlockEnum.LLM]: checkLLMValid, @@ -66,13 +70,15 @@ const checkValidFns: Record<BlockEnum, Function> = { [BlockEnum.Loop]: checkLoopValid, } as any -type Params<T> = { +export type Params<T> = { id: string data: CommonNodeType<T> defaultRunInputData: Record<string, any> moreDataForCheckValid?: any iteratorInputKey?: string loopInputKey?: string + isRunAfterSingleRun: boolean + isPaused: boolean } const varTypeToInputVarType = (type: VarType, { @@ -105,6 +111,8 @@ const useOneStepRun = <T>({ moreDataForCheckValid, iteratorInputKey, loopInputKey, + isRunAfterSingleRun, + isPaused, }: Params<T>) => { const { t } = useTranslation() const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any @@ -112,6 +120,7 @@ const useOneStepRun = <T>({ const isChatMode = useIsChatMode() const isIteration = data.type === BlockEnum.Iteration const isLoop = data.type === BlockEnum.Loop + const isStartNode = data.type === BlockEnum.Start const availableNodes = getBeforeNodesInSameBranch(id) const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id) @@ -143,6 +152,7 @@ const useOneStepRun = <T>({ } const checkValid = checkValidFns[data.type] + const appId = useAppStore.getState().appDetail?.id const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {}) const runInputDataRef = useRef(runInputData) @@ -150,26 +160,68 @@ const useOneStepRun = <T>({ runInputDataRef.current = data setRunInputData(data) }, []) - const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0 - const loopTimes = loopInputKey ? runInputData[loopInputKey].length : 0 - const [runResult, setRunResult] = useState<any>(null) - - const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate() - const [canShowSingleRun, setCanShowSingleRun] = useState(false) - const isShowSingleRun = data._isSingleRun && canShowSingleRun - const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([]) - const [loopRunResult, setLoopRunResult] = useState<NodeTracing[]>([]) + const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey]?.length : 0 + const loopTimes = loopInputKey ? runInputData[loopInputKey]?.length : 0 + const store = useStoreApi() + const workflowStore = useWorkflowStore() + const { + setShowSingleRunPanel, + } = workflowStore.getState() + const invalidLastRun = useInvalidLastRun(appId!, id) + const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null) + const { + appendNodeInspectVars, + invalidateSysVarValues, + invalidateConversationVarValues, + } = useInspectVarsCrud() + const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart + const isPausedRef = useRef(isPaused) useEffect(() => { - if (!checkValid) { - setCanShowSingleRun(true) + isPausedRef.current = isPaused + }, [isPaused]) + + const setRunResult = useCallback(async (data: NodeRunResult | null) => { + const isPaused = isPausedRef.current + + // The backend don't support pause the single run, so the frontend handle the pause state. + if(isPaused) + return + + const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded + if(!canRunLastRun) { + doSetRunResult(data) return } - if (data._isSingleRun) { - const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid) - setCanShowSingleRun(isValid) - if (!isValid) { + // run fail may also update the inspect vars when the node set the error default output. + const vars = await fetchNodeInspectVars(appId!, id) + const { getNodes } = store.getState() + const nodes = getNodes() + appendNodeInspectVars(id, vars, nodes) + if(data?.status === NodeRunningStatus.Succeeded) { + invalidLastRun() + if(isStartNode) + invalidateSysVarValues() + invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh. + } + }, [isRunAfterSingleRun, runningStatus, appId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues]) + + const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate() + const setNodeRunning = () => { + handleNodeDataUpdate({ + id, + data: { + ...data, + _singleRunningStatus: NodeRunningStatus.Running, + }, + }) + } + const checkValidWrap = () => { + if(!checkValid) + return { isValid: true, errorMessage: '' } + const res = checkValid(data, t, moreDataForCheckValid) + if(!res.isValid) { handleNodeDataUpdate({ id, data: { @@ -179,17 +231,32 @@ const useOneStepRun = <T>({ }) Toast.notify({ type: 'error', - message: errorMessage, + message: res.errorMessage, }) - } + } + return res + } + const [canShowSingleRun, setCanShowSingleRun] = useState(false) + const isShowSingleRun = data._isSingleRun && canShowSingleRun + const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([]) + const [loopRunResult, setLoopRunResult] = useState<NodeTracing[]>([]) + + useEffect(() => { + if (!checkValid) { + setCanShowSingleRun(true) + return + } + + if (data._isSingleRun) { + const { isValid } = checkValidWrap() + setCanShowSingleRun(isValid) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data._isSingleRun]) - const workflowStore = useWorkflowStore() useEffect(() => { - workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun) - }, [isShowSingleRun, workflowStore]) + setShowSingleRunPanel(!!isShowSingleRun) + }, [isShowSingleRun, setShowSingleRunPanel]) const hideSingleRun = () => { handleNodeDataUpdate({ @@ -209,7 +276,6 @@ const useOneStepRun = <T>({ }, }) } - const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed const handleRun = async (submitData: Record<string, any>) => { @@ -217,13 +283,29 @@ const useOneStepRun = <T>({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Running, }, }) let res: any + let hasError = false try { if (!isIteration && !isLoop) { - res = await singleNodeRun(appId!, id, { inputs: submitData }) as any + const isStartNode = data.type === BlockEnum.Start + const postData: Record<string, any> = {} + if(isStartNode) { + const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData + if(isChatMode) + postData.conversation_id = '' + + postData.inputs = inputs + postData.query = query + postData.files = files || [] + } + else { + postData.inputs = submitData + } + res = await singleNodeRun(appId!, id, postData) as any } else if (isIteration) { setIterationRunResult([]) @@ -235,10 +317,13 @@ const useOneStepRun = <T>({ { onWorkflowStarted: noop, onWorkflowFinished: (params) => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -311,10 +396,13 @@ const useOneStepRun = <T>({ setIterationRunResult(newIterationRunResult) }, onError: () => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -332,10 +420,13 @@ const useOneStepRun = <T>({ { onWorkflowStarted: noop, onWorkflowFinished: (params) => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -409,10 +500,13 @@ const useOneStepRun = <T>({ setLoopRunResult(newLoopRunResult) }, onError: () => { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -425,11 +519,16 @@ const useOneStepRun = <T>({ } catch (e: any) { console.error(e) + hasError = true + invalidLastRun() if (!isIteration && !isLoop) { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Failed, }, }) @@ -437,7 +536,7 @@ const useOneStepRun = <T>({ } } finally { - if (!isIteration && !isLoop) { + if (!isPausedRef.current && !isIteration && !isLoop && res) { setRunResult({ ...res, total_tokens: res.execution_metadata?.total_tokens || 0, @@ -445,11 +544,17 @@ const useOneStepRun = <T>({ }) } } - if (!isIteration && !isLoop) { + if(isPausedRef.current) + return + + if (!isIteration && !isLoop && !hasError) { + if(isPausedRef.current) + return handleNodeDataUpdate({ id, data: { ...data, + _isSingleRun: false, _singleRunningStatus: NodeRunningStatus.Succeeded, }, }) @@ -521,11 +626,19 @@ const useOneStepRun = <T>({ return varInputs } + const varSelectorsToVarInputs = (valueSelectors: ValueSelector[] | string[]): InputVar[] => { + return valueSelectors.filter(item => !!item).map((item) => { + return getInputVars([`{{#${typeof item === 'string' ? item : item.join('.')}#}}`])[0] + }) + } + return { + appId, isShowSingleRun, hideSingleRun, showSingleRun, toVarInputs, + varSelectorsToVarInputs, getInputVars, runningStatus, isCompleted, @@ -537,6 +650,8 @@ const useOneStepRun = <T>({ runResult, iterationRunResult, loopRunResult, + setNodeRunning, + checkValid: checkValidWrap, } } diff --git a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts index 839cd14026..515f2c365b 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' +import { useBoolean, useDebounceFn } from 'ahooks' import type { CodeNodeType, OutputVar, @@ -17,6 +17,7 @@ import { } from '@/app/components/workflow/hooks' import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils' +import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud' type Params<T> = { id: string @@ -34,8 +35,27 @@ function useOutputVarList<T>({ outputKeyOrders = [], onOutputKeyOrdersChange, }: Params<T>) { + const { + renameInspectVarName, + deleteInspectVar, + nodesWithInspectVars, + } = useInspectVarsCrud() + const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow() + // record the first old name value + const oldNameRecord = useRef<Record<string, string>>({}) + + const { + run: renameInspectNameWithDebounce, + } = useDebounceFn( + (id: string, newName: string) => { + const oldName = oldNameRecord.current[id] + renameInspectVarName(id, oldName, newName) + delete oldNameRecord.current[id] + }, + { wait: 500 }, + ) const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => { const newInputs = produce(inputs, (draft: any) => { draft[varKey] = newVars @@ -52,9 +72,20 @@ function useOutputVarList<T>({ onOutputKeyOrdersChange(newOutputKeyOrders) } - if (newKey) + if (newKey) { handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey]) - }, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange]) + if(!(id in oldNameRecord.current)) + oldNameRecord.current[id] = outputKeyOrders[changedIndex!] + renameInspectNameWithDebounce(id, newKey) + } + else if (changedIndex === undefined) { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === Object.keys(newVars)[0] + })?.id + if(varId) + deleteInspectVar(id, varId) + } + }, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange, handleOutVarRenameChange, id, renameInspectNameWithDebounce, nodesWithInspectVars, deleteInspectVar]) const generateNewKey = useCallback(() => { let keyIndex = Object.keys((inputs as any)[varKey]).length + 1 @@ -86,9 +117,14 @@ function useOutputVarList<T>({ }] = useBoolean(false) const [removedVar, setRemovedVar] = useState<ValueSelector>([]) const removeVarInNode = useCallback(() => { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === removedVar[1] + })?.id + if(varId) + deleteInspectVar(id, varId) removeUsedVarInNodes(removedVar) hideRemoveVarConfirm() - }, [hideRemoveVarConfirm, removeUsedVarInNodes, removedVar]) + }, [deleteInspectVar, hideRemoveVarConfirm, id, nodesWithInspectVars, removeUsedVarInNodes, removedVar]) const handleRemoveVariable = useCallback((index: number) => { const key = outputKeyOrders[index] @@ -106,7 +142,12 @@ function useOutputVarList<T>({ }) setInputs(newInputs) onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index)) - }, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, showRemoveVarConfirm, varKey]) + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === key + })?.id + if(varId) + deleteInspectVar(id, varId) + }, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, nodesWithInspectVars, deleteInspectVar, showRemoveVarConfirm, varKey]) return { handleVarsChange, diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 527b2f094d..b969702dd7 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -32,6 +32,7 @@ import { import { useNodeIterationInteractions } from '../iteration/use-interactions' import { useNodeLoopInteractions } from '../loop/use-interactions' import type { IterationNodeType } from '../iteration/types' +import CopyID from '../tool/components/copy-id' import { NodeSourceHandle, NodeTargetHandle, @@ -44,10 +45,13 @@ import AddVariablePopupWithPosition from './components/add-variable-popup-with-p import cn from '@/utils/classnames' import BlockIcon from '@/app/components/workflow/block-icon' import Tooltip from '@/app/components/base/tooltip' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' type BaseNodeProps = { children: ReactElement -} & NodeProps + id: NodeProps['id'] + data: NodeProps['data'] +} const BaseNode: FC<BaseNodeProps> = ({ id, @@ -89,6 +93,9 @@ const BaseNode: FC<BaseNodeProps> = ({ } }, [data.isInLoop, data.selected, id, handleNodeLoopChildSizeChange]) + const { hasNodeInspectVars } = useInspectVarsCrud() + const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running + const hasVarValue = hasNodeInspectVars(id) const showSelectedBorder = data.selected || data._isBundled || data._isEntering const { showRunningBorder, @@ -98,11 +105,11 @@ const BaseNode: FC<BaseNodeProps> = ({ } = useMemo(() => { return { showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder, - showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder, + showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder, showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder, showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder, } - }, [data._runningStatus, showSelectedBorder]) + }, [data._runningStatus, hasVarValue, showSelectedBorder]) const LoopIndex = useMemo(() => { let text = '' @@ -260,12 +267,12 @@ const BaseNode: FC<BaseNodeProps> = ({ data.type === BlockEnum.Loop && data._loopIndex && LoopIndex } { - (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && ( + isLoading && ( <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' /> ) } { - data._runningStatus === NodeRunningStatus.Succeeded && ( + (!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && ( <RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' /> ) } @@ -315,6 +322,11 @@ const BaseNode: FC<BaseNodeProps> = ({ </div> ) } + {data.type === BlockEnum.Tool && ( + <div className='px-3 pb-2'> + <CopyID content={data.provider_id || ''} /> + </div> + )} </div> </div> ) diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx deleted file mode 100644 index 49c61b3416..0000000000 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import type { - FC, - ReactNode, -} from 'react' -import { - cloneElement, - memo, - useCallback, -} from 'react' -import { - RiCloseLine, - RiPlayLargeLine, -} from '@remixicon/react' -import { useShallow } from 'zustand/react/shallow' -import { useTranslation } from 'react-i18next' -import NextStep from './components/next-step' -import PanelOperator from './components/panel-operator' -import HelpLink from './components/help-link' -import NodePosition from './components/node-position' -import { - DescriptionInput, - TitleInput, -} from './components/title-description-input' -import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' -import RetryOnPanel from './components/retry/retry-on-panel' -import { useResizePanel } from './hooks/use-resize-panel' -import cn from '@/utils/classnames' -import BlockIcon from '@/app/components/workflow/block-icon' -import Split from '@/app/components/workflow/nodes/_base/components/split' -import { - WorkflowHistoryEvent, - useAvailableBlocks, - useNodeDataUpdate, - useNodesInteractions, - useNodesReadOnly, - useNodesSyncDraft, - useToolIcon, - useWorkflow, - useWorkflowHistory, -} from '@/app/components/workflow/hooks' -import { - canRunBySingle, - hasErrorHandleNode, - hasRetryNode, -} from '@/app/components/workflow/utils' -import Tooltip from '@/app/components/base/tooltip' -import type { Node } from '@/app/components/workflow/types' -import { useStore as useAppStore } from '@/app/components/app/store' -import { useStore } from '@/app/components/workflow/store' - -type BasePanelProps = { - children: ReactNode -} & Node - -const BasePanel: FC<BasePanelProps> = ({ - id, - data, - children, - position, - width, - height, -}) => { - const { t } = useTranslation() - const { showMessageLogModal } = useAppStore(useShallow(state => ({ - showMessageLogModal: state.showMessageLogModal, - }))) - const showSingleRunPanel = useStore(s => s.showSingleRunPanel) - const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420 - const { - setPanelWidth, - } = useWorkflow() - const { handleNodeSelect } = useNodesInteractions() - const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const { nodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop) - const toolIcon = useToolIcon(data) - - const handleResize = useCallback((width: number) => { - setPanelWidth(width) - }, [setPanelWidth]) - - const { - triggerRef, - containerRef, - } = useResizePanel({ - direction: 'horizontal', - triggerDirection: 'left', - minWidth: 420, - maxWidth: 720, - onResize: handleResize, - }) - - const { saveStateToHistory } = useWorkflowHistory() - - const { - handleNodeDataUpdate, - handleNodeDataUpdateWithSyncDraft, - } = useNodeDataUpdate() - - const handleTitleBlur = useCallback((title: string) => { - handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) - saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) - }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) - const handleDescriptionChange = useCallback((desc: string) => { - handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) - saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) - }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) - - return ( - <div className={cn( - 'relative mr-2 h-full', - showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all', - )}> - <div - ref={triggerRef} - className='absolute -left-2 top-1/2 h-6 w-3 -translate-y-1/2 cursor-col-resize resize-x'> - <div className='h-6 w-1 rounded-sm bg-divider-regular'></div> - </div> - <div - ref={containerRef} - className={cn('h-full rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')} - style={{ - width: `${panelWidth}px`, - }} - > - <div className='sticky top-0 z-10 border-b-[0.5px] border-divider-regular bg-components-panel-bg'> - <div className='flex items-center px-4 pb-1 pt-4'> - <BlockIcon - className='mr-1 shrink-0' - type={data.type} - toolIcon={toolIcon} - size='md' - /> - <TitleInput - value={data.title || ''} - onBlur={handleTitleBlur} - /> - <div className='flex shrink-0 items-center text-text-tertiary'> - { - canRunBySingle(data.type) && !nodesReadOnly && ( - <Tooltip - popupContent={t('workflow.panel.runThisStep')} - popupClassName='mr-1' - > - <div - className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover' - onClick={() => { - handleNodeDataUpdate({ id, data: { _isSingleRun: true } }) - handleSyncWorkflowDraft(true) - }} - > - <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' /> - </div> - </Tooltip> - ) - } - <NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition> - <HelpLink nodeType={data.type} /> - <PanelOperator id={id} data={data} showHelpLink={false} /> - <div className='mx-3 h-3.5 w-[1px] bg-divider-regular' /> - <div - className='flex h-6 w-6 cursor-pointer items-center justify-center' - onClick={() => handleNodeSelect(id, true)} - > - <RiCloseLine className='h-4 w-4 text-text-tertiary' /> - </div> - </div> - </div> - <div className='p-2'> - <DescriptionInput - value={data.desc || ''} - onChange={handleDescriptionChange} - /> - </div> - </div> - <div> - {cloneElement(children as any, { id, data })} - </div> - <Split /> - { - hasRetryNode(data.type) && ( - <RetryOnPanel - id={id} - data={data} - /> - ) - } - { - hasErrorHandleNode(data.type) && ( - <ErrorHandleOnPanel - id={id} - data={data} - /> - ) - } - { - !!availableNextBlocks.length && ( - <div className='border-t-[0.5px] border-divider-regular p-4'> - <div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'> - {t('workflow.panel.nextStep').toLocaleUpperCase()} - </div> - <div className='system-xs-regular mb-2 text-text-tertiary'> - {t('workflow.panel.addNextStep')} - </div> - <NextStep selectedNode={{ id, data } as Node} /> - </div> - ) - } - </div> - </div> - ) -} - -export default memo(BasePanel) diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index b94258855a..4ff0cd780d 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import classNames from '@/utils/classnames' import { memo, useMemo, useRef, useState } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { useTranslation } from 'react-i18next' import { Group } from '@/app/components/base/icons/src/vender/other' +import AppIcon from '@/app/components/base/app-icon' type Status = 'not-installed' | 'not-authorized' | undefined @@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() - const isDataReady = !!buildInTools && !!customTools && !!workflowTools + const { data: mcpTools } = useAllMCPTools() + const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools const currentProvider = useMemo(() => { - const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] + const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])] return mergedTools.find((toolWithProvider) => { - return toolWithProvider.name === providerName + return toolWithProvider.name === providerName || toolWithProvider.id === providerName }) - }, [buildInTools, customTools, providerName, workflowTools]) + }, [buildInTools, customTools, providerName, workflowTools, mcpTools]) + const providerNameParts = providerName.split('/') const author = providerNameParts[0] const name = providerNameParts[1] const icon = useMemo(() => { if (!isDataReady) return '' - if (currentProvider) return currentProvider.icon as string + if (currentProvider) return currentProvider.icon const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`) return iconFromMarketPlace }, [author, currentProvider, name, isDataReady]) @@ -58,23 +61,36 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { > <div className={classNames( - 'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]', + 'relative flex size-5 items-center justify-center rounded-[6px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge', )} ref={containerRef} > - {(!iconFetchError && isDataReady) - - ? <img - src={icon} - alt='tool icon' - className={classNames( - 'w-full h-full size-3.5 object-cover', - notSuccess && 'opacity-50', - )} - onError={() => setIconFetchError(true)} - /> - : <Group className="h-3 w-3 opacity-35" /> - } + {(() => { + if (iconFetchError || !icon) + return <Group className="h-3 w-3 opacity-35" /> + if (typeof icon === 'string') { + return <img + src={icon} + alt='tool icon' + className={classNames( + 'size-3.5 h-full w-full object-cover', + notSuccess && 'opacity-50', + )} + onError={() => setIconFetchError(true)} + /> + } + if (typeof icon === 'object') { + return <AppIcon + className={classNames( + 'size-3.5 h-full w-full object-cover', + notSuccess && 'opacity-50', + )} + icon={icon?.content} + background={icon?.background} + /> + } + return <Group className="h-3 w-3 opacity-35" /> + })()} {indicator && <Indicator color={indicator} className="absolute right-[-1px] top-[-1px]" />} </div> </Tooltip> diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts index d80def7bd2..4f68cfe87c 100644 --- a/web/app/components/workflow/nodes/agent/default.ts +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n' const nodeDefault: NodeDefault<AgentNodeType> = { defaultValue: { + version: '2', }, getAvailablePrevNodes(isChatMode) { return isChatMode @@ -60,15 +61,28 @@ const nodeDefault: NodeDefault<AgentNodeType> = { const schemas = toolValue.schemas || [] const userSettings = toolValue.settings const reasoningConfig = toolValue.parameters + const version = payload.version schemas.forEach((schema: any) => { if (schema?.required) { - if (schema.form === 'form' && !userSettings[schema.name]?.value) { + if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), } } - if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) { + if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) { + return { + isValid: false, + errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), + } + } + if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) { + return { + isValid: false, + errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), + } + } + if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index 57ad2a0b81..a2190317af 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -54,9 +54,9 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { const field = param.name const value = inputs.agent_parameters?.[field]?.value if (value) { - (value as unknown as any[]).forEach((item) => { + (value as unknown as any[]).forEach((item, idx) => { tools.push({ - id: `${param.name}-${i}`, + id: `${param.name}-${idx}`, providerName: item.provider_name, }) }) @@ -104,7 +104,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => { {t('workflow.nodes.agent.toolbox')} </GroupLabel>}> <div className='grid grid-cols-10 gap-0.5'> - {tools.map(tool => <ToolIcon {...tool} key={tool.id} />)} + {tools.map((tool, i) => <ToolIcon {...tool} key={tool.id + i} />)} </div> </Group>} </div> diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index f92e92dbcb..6741453944 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { memo, useMemo } from 'react' +import { memo } from 'react' import type { NodePanelProps } from '../../types' import { AgentFeature, type AgentNodeType } from './types' import Field from '../_base/components/field' @@ -9,16 +9,10 @@ import { useTranslation } from 'react-i18next' import OutputVars, { VarItem } from '../_base/components/output-vars' import type { StrategyParamItem } from '@/app/components/plugins/types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' -import formatTracing from '@/app/components/workflow/run/utils/format-log' -import { useLogs } from '@/app/components/workflow/run/hooks' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import { toType } from '@/app/components/tools/utils/to-form-schema' import { useStore } from '../../store' import Split from '../_base/components/split' import MemoryConfig from '../_base/components/memory-config' - const i18nPrefix = 'workflow.nodes.agent' export function strategyParamToCredientialForm(param: StrategyParamItem): CredentialFormSchema { @@ -42,44 +36,13 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { availableNodesWithParent, availableVars, readOnly, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - runInputData, - setRunInputData, - varInputs, outputSchema, handleMemoryChange, + canChooseMCPTool, } = useConfig(props.id, props.data) const { t } = useTranslation() - const nodeInfo = useMemo(() => { - if (!runResult) - return - return formatTracing([runResult], t)[0] - }, [runResult, t]) - const logsParams = useLogs() - const singleRunForms = (() => { - const forms: FormProps[] = [] - - if (varInputs.length > 0) { - forms.push( - { - label: t(`${i18nPrefix}.singleRun.variable`)!, - inputs: varInputs, - values: runInputData, - onChange: setRunInputData, - }, - ) - } - - return forms - })() const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey) - return <div className='my-2'> <Field required @@ -93,6 +56,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { agent_strategy_label: inputs.agent_strategy_label!, agent_output_schema: inputs.output_schema, plugin_unique_identifier: inputs.plugin_unique_identifier!, + meta: inputs.meta, } : undefined} onStrategyChange={(strategy) => { setInputs({ @@ -102,6 +66,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { agent_strategy_label: strategy?.agent_strategy_label, output_schema: strategy!.agent_output_schema, plugin_unique_identifier: strategy!.plugin_unique_identifier, + meta: strategy?.meta, }) resetEditor(Date.now()) }} @@ -111,6 +76,7 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { nodeOutputVars={availableVars} availableNodes={availableNodesWithParent} nodeId={props.id} + canChooseMCPTool={canChooseMCPTool} /> </Field> <div className='px-4 py-2'> @@ -154,21 +120,6 @@ const AgentPanel: FC<NodePanelProps<AgentNodeType>> = (props) => { ))} </OutputVars> </div> - { - isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - nodeType={inputs.type} - onHide={hideSingleRun} - forms={singleRunForms} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - {...logsParams} - result={<ResultPanel {...runResult} nodeInfo={nodeInfo} showSteps={false} {...logsParams} />} - /> - ) - } </div> } diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts index ca8bb5e71d..5a13a4a4f3 100644 --- a/web/app/components/workflow/nodes/agent/types.ts +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -1,14 +1,17 @@ import type { CommonNodeType, Memory } from '@/app/components/workflow/types' import type { ToolVarInputs } from '../tool/types' +import type { PluginMeta } from '@/app/components/plugins/types' export type AgentNodeType = CommonNodeType & { agent_strategy_provider_name?: string agent_strategy_name?: string agent_strategy_label?: string agent_parameters?: ToolVarInputs + meta?: PluginMeta output_schema: Record<string, any> plugin_unique_identifier?: string memory?: Memory + version?: string } export enum AgentFeature { diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index 5ecf1657f6..a8b80c0348 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -1,13 +1,12 @@ import { useStrategyProviderDetail } from '@/service/use-strategy' import useNodeCrud from '../_base/hooks/use-node-crud' import useVarList from '../_base/hooks/use-var-list' -import useOneStepRun from '../_base/hooks/use-one-step-run' import type { AgentNodeType } from './types' import { useIsChatMode, useNodesReadOnly, } from '@/app/components/workflow/hooks' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { type ToolVarInputs, VarType } from '../tool/types' import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins' import type { Memory, Var } from '../../types' @@ -15,6 +14,8 @@ import { VarType as VarKindType } from '../../types' import useAvailableVarList from '../_base/hooks/use-available-var-list' import produce from 'immer' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { isSupportMCP } from '@/utils/plugin-version-feature' +import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' export type StrategyStatus = { plugin: { @@ -87,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) const formData = useMemo(() => { const paramNameList = (currentStrategy?.parameters || []).map(item => item.name) - return Object.fromEntries( + const res = Object.fromEntries( Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => { return [key, value.value] }), ) + return res }, [inputs.agent_parameters, currentStrategy?.parameters]) const getParamVarType = useCallback((paramName: string) => { @@ -116,6 +118,42 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) } + const formattingToolData = (data: any) => { + const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any)) + const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true) + const res = produce(data, (draft: any) => { + draft.settings = settingValues + draft.parameters = paramValues + }) + return res + } + + const formattingLegacyData = () => { + if (inputs.version) + return inputs + const newData = produce(inputs, (draft) => { + const schemas = currentStrategy?.parameters || [] + Object.keys(draft.agent_parameters || {}).forEach((key) => { + const targetSchema = schemas.find(schema => schema.name === key) + if (targetSchema?.type === FormTypeEnum.toolSelector) + draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value) + if (targetSchema?.type === FormTypeEnum.multiToolSelector) + draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool)) + }) + draft.version = '2' + }) + return newData + } + + // formatting legacy data + useEffect(() => { + if (!currentStrategy) + return + const newData = formattingLegacyData() + setInputs(newData) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStrategy]) + // vars const filterMemoryPromptVar = useCallback((varPayload: Var) => { @@ -141,35 +179,6 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) // single run - const { - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - getInputVars, - } = useOneStepRun<AgentNodeType>({ - id, - data: inputs, - defaultRunInputData: {}, - }) - const allVarStrArr = (() => { - const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => { - return formData[item.name] - }) || [] - - return arr - })() - const varInputs = (() => { - const vars = getInputVars(allVarStrArr) - - return vars - })() const outputSchema = useMemo(() => { const res: any[] = [] @@ -209,21 +218,10 @@ const useConfig = (id: string, payload: AgentNodeType) => { pluginDetail: pluginDetail.data?.plugins.at(0), availableVars, availableNodesWithParent, - - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - varInputs, outputSchema, handleMemoryChange, isChatMode, + canChooseMCPTool: isSupportMCP(inputs.meta?.version), } } diff --git a/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts b/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts new file mode 100644 index 0000000000..5ddc24b2f2 --- /dev/null +++ b/web/app/components/workflow/nodes/agent/use-single-run-form-params.ts @@ -0,0 +1,90 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { AgentNodeType } from './types' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import { useStrategyInfo } from './use-config' +import type { NodeTracing } from '@/types/workflow' +import formatTracing from '@/app/components/workflow/run/utils/format-log' + +type Params = { + id: string, + payload: AgentNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + runResult: NodeTracing +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + getInputVars, + setRunInputData, + runResult, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud<AgentNodeType>(id, payload) + + const formData = useMemo(() => { + return Object.fromEntries( + Object.entries(inputs.agent_parameters || {}).map(([key, value]) => { + return [key, value.value] + }), + ) + }, [inputs.agent_parameters]) + + const { + strategy: currentStrategy, + } = useStrategyInfo( + inputs.agent_strategy_provider_name, + inputs.agent_strategy_name, + ) + + const allVarStrArr = (() => { + const arr = currentStrategy?.parameters.filter(item => item.type === 'string').map((item) => { + return formData[item.name] + }) || [] + return arr + })() + + const varInputs = getInputVars?.(allVarStrArr) + + const forms = useMemo(() => { + const forms: FormProps[] = [] + + if (varInputs!.length > 0) { + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: varInputs!, + values: runInputData, + onChange: setRunInputData, + }, + ) + } + return forms + }, [runInputData, setRunInputData, t, varInputs]) + + const nodeInfo = useMemo(() => { + if (!runResult) + return + return formatTracing([runResult], t)[0] + }, [runResult, t]) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + nodeInfo, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index f34a1435ad..b19d5903a6 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -52,6 +52,7 @@ const VarList: FC<Props> = ({ const newList = produce(list, (draft) => { draft[index].variable_selector = value as ValueSelector draft[index].operation = WriteMode.overwrite + draft[index].input_type = AssignerNodeInputType.variable draft[index].value = undefined }) onChange(newList, value as ValueSelector) diff --git a/web/app/components/workflow/nodes/assigner/types.ts b/web/app/components/workflow/nodes/assigner/types.ts index 85d2b2850f..22f37bb7cd 100644 --- a/web/app/components/workflow/nodes/assigner/types.ts +++ b/web/app/components/workflow/nodes/assigner/types.ts @@ -30,3 +30,5 @@ export type AssignerNodeType = CommonNodeType & { version?: '1' | '2' items: AssignerNodeOperation[] } + +export const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide] diff --git a/web/app/components/workflow/nodes/assigner/use-config.ts b/web/app/components/workflow/nodes/assigner/use-config.ts index cbd5475483..c42dd67b37 100644 --- a/web/app/components/workflow/nodes/assigner/use-config.ts +++ b/web/app/components/workflow/nodes/assigner/use-config.ts @@ -5,6 +5,7 @@ import { VarType } from '../../types' import type { ValueSelector, Var } from '../../types' import { WriteMode } from './types' import type { AssignerNodeOperation, AssignerNodeType } from './types' +import { writeModeTypesNum } from './types' import { useGetAvailableVars } from './hooks' import { convertV1ToV2 } from './utils' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' @@ -71,7 +72,6 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { const writeModeTypesArr = [WriteMode.overwrite, WriteMode.clear, WriteMode.append, WriteMode.extend, WriteMode.removeFirst, WriteMode.removeLast] const writeModeTypes = [WriteMode.overwrite, WriteMode.clear, WriteMode.set] - const writeModeTypesNum = [WriteMode.increment, WriteMode.decrement, WriteMode.multiply, WriteMode.divide] const getToAssignedVarType = useCallback((assignedVarType: VarType, write_mode: WriteMode) => { if (write_mode === WriteMode.overwrite || write_mode === WriteMode.increment || write_mode === WriteMode.decrement diff --git a/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts b/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts new file mode 100644 index 0000000000..7ff31d91c7 --- /dev/null +++ b/web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts @@ -0,0 +1,55 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { type AssignerNodeType, WriteMode } from './types' +import { writeModeTypesNum } from './types' + +type Params = { + id: string, + payload: AssignerNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + setRunInputData, + varSelectorsToVarInputs, +}: Params) => { + const { inputs } = useNodeCrud<AssignerNodeType>(id, payload) + + const vars = (inputs.items ?? []).filter((item) => { + return item.operation !== WriteMode.clear && item.operation !== WriteMode.set + && item.operation !== WriteMode.removeFirst && item.operation !== WriteMode.removeLast + && !writeModeTypesNum.includes(item.operation) + }).map(item => item.value as ValueSelector) + + const forms = useMemo(() => { + const varInputs = varSelectorsToVarInputs(vars) + + return [ + { + inputs: varInputs, + values: runInputData, + onChange: setRunInputData, + }, + ] + }, [runInputData, setRunInputData, varSelectorsToVarInputs, vars]) + + const getDependentVars = () => { + return vars + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/code/panel.tsx b/web/app/components/workflow/nodes/code/panel.tsx index a0b7535f89..f928c13bd6 100644 --- a/web/app/components/workflow/nodes/code/panel.tsx +++ b/web/app/components/workflow/nodes/code/panel.tsx @@ -14,8 +14,7 @@ import Split from '@/app/components/workflow/nodes/_base/components/split' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import SyncButton from '@/app/components/base/button/sync-button' const i18nPrefix = 'workflow.nodes.code' const codeLanguages = [ @@ -42,6 +41,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({ handleVarListChange, handleAddVariable, handleRemoveVariable, + handleSyncFunctionSignature, handleCodeChange, handleCodeLanguageChange, handleVarsChange, @@ -50,16 +50,6 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({ isShowRemoveVarConfirm, hideRemoveVarConfirm, onRemoveVarConfirm, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - varInputs, - inputVarValues, - setInputVarValues, } = useConfig(id, data) const handleGeneratedCode = (value: string) => { @@ -80,7 +70,12 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({ <Field title={t(`${i18nPrefix}.inputVars`)} operations={ - !readOnly ? <AddButton onClick={handleAddVariable} /> : undefined + !readOnly ? ( + <div className="flex gap-2"> + <SyncButton popupContent={t(`${i18nPrefix}.syncFunctionSignature`)} onClick={handleSyncFunctionSignature} /> + <AddButton onClick={handleAddVariable} /> + </div> + ) : undefined } > <VarList @@ -128,25 +123,6 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({ /> </Field> </div> - { - isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[ - { - inputs: varInputs, - values: inputVarValues, - onChange: setInputVarValues, - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - ) - } <RemoveEffectVarConfirm isShow={isShowRemoveVarConfirm} onCancel={hideRemoveVarConfirm} diff --git a/web/app/components/workflow/nodes/code/use-config.ts b/web/app/components/workflow/nodes/code/use-config.ts index 13b8962439..9b0c14e529 100644 --- a/web/app/components/workflow/nodes/code/use-config.ts +++ b/web/app/components/workflow/nodes/code/use-config.ts @@ -8,7 +8,6 @@ import { useStore } from '../../store' import type { CodeNodeType, OutputVar } from './types' import { CodeLanguage } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { fetchNodeDefault } from '@/service/workflow' import { useStore as useAppStore } from '@/app/components/app/store' import { @@ -61,7 +60,7 @@ const useConfig = (id: string, payload: CodeNodeType) => { }) syncOutputKeyOrders(defaultConfig.outputs) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleCodeChange = useCallback((code: string) => { @@ -85,6 +84,65 @@ const useConfig = (id: string, payload: CodeNodeType) => { setInputs(newInputs) }, [allLanguageDefault, inputs, setInputs]) + const handleSyncFunctionSignature = useCallback(() => { + const generateSyncSignatureCode = (code: string) => { + let mainDefRe + let newMainDef + if (inputs.code_language === CodeLanguage.javascript) { + mainDefRe = /function\s+main\b\s*\([\s\S]*?\)/g + newMainDef = 'function main({{var_list}})' + let param_list = inputs.variables?.map(item => item.variable).join(', ') || '' + param_list = param_list ? `{${param_list}}` : '' + newMainDef = newMainDef.replace('{{var_list}}', param_list) + } + + else if (inputs.code_language === CodeLanguage.python3) { + mainDefRe = /def\s+main\b\s*\([\s\S]*?\)/g + const param_list = [] + for (const item of inputs.variables) { + let param = item.variable + let param_type = '' + switch (item.value_type) { + case VarType.string: + param_type = ': str' + break + case VarType.number: + param_type = ': float' + break + case VarType.object: + param_type = ': dict' + break + case VarType.array: + param_type = ': list' + break + case VarType.arrayNumber: + param_type = ': list[float]' + break + case VarType.arrayString: + param_type = ': list[str]' + break + case VarType.arrayObject: + param_type = ': list[dict]' + break + } + param += param_type + param_list.push(`${param}`) + } + + newMainDef = `def main(${param_list.join(', ')})` + } + else { return code } + + const newCode = code.replace(mainDefRe, newMainDef) + return newCode + } + + const newInputs = produce(inputs, (draft) => { + draft.code = generateSyncSignatureCode(draft.code) + }) + setInputs(newInputs) + }, [inputs, setInputs]) + const { handleVarsChange, handleAddVariable: handleAddOutputVariable, @@ -104,38 +162,6 @@ const useConfig = (id: string, payload: CodeNodeType) => { return [VarType.string, VarType.number, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject, VarType.file, VarType.arrayFile].includes(varPayload.type) }, []) - // single run - const { - isShowSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - isCompleted, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun<CodeNodeType>({ - id, - data: inputs, - defaultRunInputData: {}, - }) - - const varInputs = toVarInputs(inputs.variables) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - setRunInputData(newPayload) - }, [setRunInputData]) const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => { const newInputs = produce(inputs, (draft) => { draft.code = code @@ -152,6 +178,7 @@ const useConfig = (id: string, payload: CodeNodeType) => { handleVarListChange, handleAddVariable, handleRemoveVariable, + handleSyncFunctionSignature, handleCodeChange, handleCodeLanguageChange, handleVarsChange, @@ -160,17 +187,6 @@ const useConfig = (id: string, payload: CodeNodeType) => { isShowRemoveVarConfirm, hideRemoveVarConfirm, onRemoveVarConfirm, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, handleCodeAndVarsChange, } } diff --git a/web/app/components/workflow/nodes/code/use-single-run-form-params.ts b/web/app/components/workflow/nodes/code/use-single-run-form-params.ts new file mode 100644 index 0000000000..9714e55fff --- /dev/null +++ b/web/app/components/workflow/nodes/code/use-single-run-form-params.ts @@ -0,0 +1,65 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { CodeNodeType } from './types' + +type Params = { + id: string, + payload: CodeNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + toVarInputs, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud<CodeNodeType>(id, payload) + + const varInputs = toVarInputs(inputs.variables) + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return payload.variables.map(v => v.value_selector) + } + + const getDependentVar = (variable: string) => { + const varItem = payload.variables.find(v => v.variable === variable) + if (varItem) + return varItem.value_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/document-extractor/panel.tsx b/web/app/components/workflow/nodes/document-extractor/panel.tsx index 5ed1425778..a91608c717 100644 --- a/web/app/components/workflow/nodes/document-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/document-extractor/panel.tsx @@ -11,11 +11,9 @@ import useConfig from './use-config' import type { DocExtractorNodeType } from './types' import { fetchSupportFileTypes } from '@/service/datasets' import Field from '@/app/components/workflow/nodes/_base/components/field' -import { BlockEnum, InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import { BlockEnum, type NodePanelProps } from '@/app/components/workflow/types' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n/language' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.docExtractor' @@ -48,15 +46,6 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({ inputs, handleVarChanges, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - files, - setFiles, } = useConfig(id, data) return ( @@ -93,30 +82,6 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({ /> </OutputVars> </div> - { - isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[ - { - inputs: [{ - label: t(`${i18nPrefix}.inputVar`)!, - variable: 'files', - type: InputVarType.multiFiles, - required: true, - }], - values: { files }, - onChange: keyValue => setFiles(keyValue.files), - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - ) - } </div> ) } diff --git a/web/app/components/workflow/nodes/document-extractor/use-config.ts b/web/app/components/workflow/nodes/document-extractor/use-config.ts index 8ceb153874..43f3e71fa2 100644 --- a/web/app/components/workflow/nodes/document-extractor/use-config.ts +++ b/web/app/components/workflow/nodes/document-extractor/use-config.ts @@ -1,12 +1,10 @@ import { useCallback, useMemo } from 'react' import produce from 'immer' import { useStoreApi } from 'reactflow' - import type { ValueSelector, Var } from '../../types' -import { InputVarType, VarType } from '../../types' +import { VarType } from '../../types' import type { DocExtractorNodeType } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useIsChatMode, useNodesReadOnly, @@ -58,53 +56,11 @@ const useConfig = (id: string, payload: DocExtractorNodeType) => { setInputs(newInputs) }, [getType, inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun<DocExtractorNodeType>({ - id, - data: inputs, - defaultRunInputData: { files: [] }, - }) - const varInputs = [{ - label: inputs.title, - variable: 'files', - type: InputVarType.multiFiles, - required: true, - }] - - const files = runInputData.files - const setFiles = useCallback((newFiles: []) => { - setRunInputData({ - ...runInputData, - files: newFiles, - }) - }, [runInputData, setRunInputData]) - return { readOnly, inputs, filterVar, handleVarChanges, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - isCompleted, - handleRun, - handleStop, - varInputs, - files, - setFiles, - runResult, } } diff --git a/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts new file mode 100644 index 0000000000..3b249cd210 --- /dev/null +++ b/web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts @@ -0,0 +1,64 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import type { DocExtractorNodeType } from './types' +import { useTranslation } from 'react-i18next' +import { InputVarType } from '@/app/components/workflow/types' + +const i18nPrefix = 'workflow.nodes.docExtractor' + +type Params = { + id: string, + payload: DocExtractorNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const files = runInputData.files + const setFiles = useCallback((newFiles: []) => { + setRunInputData({ + ...runInputData, + files: newFiles, + }) + }, [runInputData, setRunInputData]) + + const forms = useMemo(() => { + return [ + { + inputs: [{ + label: t(`${i18nPrefix}.inputVar`)!, + variable: 'files', + type: InputVarType.multiFiles, + required: true, + }], + values: { files }, + onChange: (keyValue: Record<string, any>) => setFiles(keyValue.files), + }, + ] + }, [files, setFiles, t]) + + const getDependentVars = () => { + return [payload.variable_selector] + } + + const getDependentVar = (variable: string) => { + if(variable === 'files') + return payload.variable_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/http/default.ts b/web/app/components/workflow/nodes/http/default.ts index 1bd584eeb9..3f9df0178d 100644 --- a/web/app/components/workflow/nodes/http/default.ts +++ b/web/app/components/workflow/nodes/http/default.ts @@ -22,6 +22,7 @@ const nodeDefault: NodeDefault<HttpNodeType> = { type: BodyType.none, data: [], }, + ssl_verify: true, timeout: { max_connect_timeout: 0, max_read_timeout: 0, diff --git a/web/app/components/workflow/nodes/http/node.tsx b/web/app/components/workflow/nodes/http/node.tsx index ad4b5f58ff..aa1912bd59 100644 --- a/web/app/components/workflow/nodes/http/node.tsx +++ b/web/app/components/workflow/nodes/http/node.tsx @@ -13,7 +13,7 @@ const Node: FC<NodeProps<HttpNodeType>> = ({ return ( <div className='mb-1 px-3 py-1'> - <div className='flex items-start rounded-md bg-workflow-block-parma-bg p-1'> + <div className='flex items-center justify-start rounded-md bg-workflow-block-parma-bg p-1'> <div className='flex h-4 shrink-0 items-center rounded bg-components-badge-white-to-dark px-1 text-xs font-semibold uppercase text-text-secondary'>{method}</div> <div className='pl-1 pt-1'> <ReadonlyInputWithSelectVar diff --git a/web/app/components/workflow/nodes/http/panel.tsx b/web/app/components/workflow/nodes/http/panel.tsx index 60f3de81c0..b994910ea0 100644 --- a/web/app/components/workflow/nodes/http/panel.tsx +++ b/web/app/components/workflow/nodes/http/panel.tsx @@ -10,14 +10,13 @@ import type { HttpNodeType } from './types' import Timeout from './components/timeout' import CurlPanel from './components/curl-panel' import cn from '@/utils/classnames' +import Switch from '@/app/components/base/switch' import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import { Settings01 } from '@/app/components/base/icons/src/vender/line/general' import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.http' @@ -45,20 +44,11 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ hideAuthorization, setAuthorization, setTimeout, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, isShowCurlPanel, showCurlPanel, hideCurlPanel, handleCurlImport, + handleSSLVerifyChange, } = useConfig(id, data) // To prevent prompt editor in body not update data. if (!isDataReady) @@ -136,6 +126,18 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ onChange={setBody} /> </Field> + <Field + title={t(`${i18nPrefix}.verifySSL.title`)} + tooltip={t(`${i18nPrefix}.verifySSL.warningTooltip`)} + operations={ + <Switch + defaultValue={!!inputs.ssl_verify} + onChange={handleSSLVerifyChange} + size='md' + disabled={readOnly} + /> + }> + </Field> </div> <Split /> <Timeout @@ -180,24 +182,6 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ </> </OutputVars> </div> - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - nodeType={inputs.type} - onHide={hideSingleRun} - forms={[ - { - inputs: varInputs, - values: inputVarValues, - onChange: setInputVarValues, - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} {(isShowCurlPanel && !readOnly) && ( <CurlPanel nodeId={id} diff --git a/web/app/components/workflow/nodes/http/types.ts b/web/app/components/workflow/nodes/http/types.ts index f1937ec5bd..0e3fbc0d91 100644 --- a/web/app/components/workflow/nodes/http/types.ts +++ b/web/app/components/workflow/nodes/http/types.ts @@ -81,4 +81,5 @@ export type HttpNodeType = CommonNodeType & { body: Body authorization: Authorization timeout: Timeout + ssl_verify?: boolean } diff --git a/web/app/components/workflow/nodes/http/use-config.ts b/web/app/components/workflow/nodes/http/use-config.ts index 73c84c369f..a539a88a8b 100644 --- a/web/app/components/workflow/nodes/http/use-config.ts +++ b/web/app/components/workflow/nodes/http/use-config.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import produce from 'immer' import { useBoolean } from 'ahooks' import useVarList from '../_base/hooks/use-var-list' @@ -9,7 +9,6 @@ import { type Authorization, type Body, BodyType, type HttpNodeType, type Method import useKeyValueList from './hooks/use-key-value-list' import { transformToBodyPayload } from './utils' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useNodesReadOnly, } from '@/app/components/workflow/hooks' @@ -125,55 +124,6 @@ const useConfig = (id: string, payload: HttpNodeType) => { return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) }, []) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun<HttpNodeType>({ - id, - data: inputs, - defaultRunInputData: {}, - }) - - const fileVarInputs = useMemo(() => { - if (!Array.isArray(inputs.body.data)) - return '' - - const res = inputs.body.data - .filter(item => item.file?.length) - .map(item => item.file ? `{{#${item.file.join('.')}#}}` : '') - .join(' ') - return res - }, [inputs.body.data]) - - const varInputs = getInputVars([ - inputs.url, - inputs.headers, - inputs.params, - typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''), - fileVarInputs, - ]) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - setRunInputData(newPayload) - }, [setRunInputData]) - // curl import panel const [isShowCurlPanel, { setTrue: showCurlPanel, @@ -191,6 +141,13 @@ const useConfig = (id: string, payload: HttpNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) + const handleSSLVerifyChange = useCallback((checked: boolean) => { + const newInputs = produce(inputs, (draft: HttpNodeType) => { + draft.ssl_verify = checked + }) + setInputs(newInputs) + }, [inputs, setInputs]) + return { readOnly, isDataReady, @@ -214,22 +171,14 @@ const useConfig = (id: string, payload: HttpNodeType) => { toggleIsParamKeyValueEdit, // body setBody, + // ssl verify + handleSSLVerifyChange, // authorization isShowAuthorization, showAuthorization, hideAuthorization, setAuthorization, setTimeout, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, // curl import isShowCurlPanel, showCurlPanel, diff --git a/web/app/components/workflow/nodes/http/use-single-run-form-params.ts b/web/app/components/workflow/nodes/http/use-single-run-form-params.ts new file mode 100644 index 0000000000..c5d65634c4 --- /dev/null +++ b/web/app/components/workflow/nodes/http/use-single-run-form-params.ts @@ -0,0 +1,74 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { HttpNodeType } from './types' + +type Params = { + id: string, + payload: HttpNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + getInputVars, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud<HttpNodeType>(id, payload) + + const fileVarInputs = useMemo(() => { + if (!Array.isArray(inputs.body.data)) + return '' + + const res = inputs.body.data + .filter(item => item.file?.length) + .map(item => item.file ? `{{#${item.file.join('.')}#}}` : '') + .join(' ') + return res + }, [inputs.body.data]) + const varInputs = getInputVars([ + inputs.url, + inputs.headers, + inputs.params, + typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''), + fileVarInputs, + ]) + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx index ef94f7c82e..eabc10b168 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx @@ -25,6 +25,7 @@ import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../cons import ConditionWrap from '../condition-wrap' import ConditionOperator from './condition-operator' import ConditionInput from './condition-input' +import { useWorkflowStore } from '@/app/components/workflow/store' import ConditionVarSelector from './condition-var-selector' import type { @@ -37,6 +38,8 @@ import { VarType } from '@/app/components/workflow/types' import cn from '@/utils/classnames' import { SimpleSelect as Select } from '@/app/components/base/select' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow' const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName' type ConditionItemProps = { @@ -82,10 +85,15 @@ const ConditionItem = ({ filterVar, }: ConditionItemProps) => { const { t } = useTranslation() - + const isChatMode = useIsChatMode() const [isHovered, setIsHovered] = useState(false) const [open, setOpen] = useState(false) + const workflowStore = useWorkflowStore() + const { + setControlPromptEditorRerenderKey, + } = workflowStore.getState() + const doUpdateCondition = useCallback((newCondition: Condition) => { if (isSubVariableKey) onUpdateSubVariableCondition?.(caseId, conditionId, condition.id, newCondition) @@ -120,6 +128,7 @@ const ConditionItem = ({ }, [condition, doUpdateCondition]) const isSubVariable = condition.varType === VarType.arrayFile && [ComparisonOperator.contains, ComparisonOperator.notContains, ComparisonOperator.allOf].includes(condition.comparison_operator!) + const fileAttr = useMemo(() => { if (file) return file @@ -194,15 +203,22 @@ const ConditionItem = ({ }, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition]) const handleVarChange = useCallback((valueSelector: ValueSelector, varItem: Var) => { + const resolvedVarType = getVarType({ + valueSelector, + availableNodes, + isChatMode, + }) + const newCondition = produce(condition, (draft) => { draft.variable_selector = valueSelector - draft.varType = varItem.type + draft.varType = resolvedVarType draft.value = '' - draft.comparison_operator = getOperators(varItem.type)[0] + draft.comparison_operator = getOperators(resolvedVarType)[0] + setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) }) doUpdateCondition(newCondition) setOpen(false) - }, [condition, doUpdateCondition]) + }, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey]) return ( <div className={cn('mb-1 flex last-of-type:mb-0', className)}> diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx index 9036e04d3b..a2b3cb7589 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx @@ -69,7 +69,7 @@ const ConditionOperator = ({ <RiArrowDownSLine className='ml-1 h-3.5 w-3.5' /> </Button> </PortalToFollowElemTrigger> - <PortalToFollowElemContent className='z-10'> + <PortalToFollowElemContent className='z-[11]'> <div className='rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'> { options.map(option => ( diff --git a/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts b/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts new file mode 100644 index 0000000000..f61f2846c3 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts @@ -0,0 +1,166 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useCallback } from 'react' +import type { CaseItem, Condition, IfElseNodeType } from './types' + +type Params = { + id: string, + payload: IfElseNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, + getInputVars, + varSelectorsToVarInputs, +}: Params) => { + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarSelectorsFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarSelectorsFromCondition = (condition: Condition) => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition)) + return vars + } + + const getInputVarsFromCase = (caseItem: CaseItem): InputVar[] => { + const vars: InputVar[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getInputVarsFromConditionValue(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getInputVarsFromConditionValue = (condition: Condition): InputVar[] => { + const vars: InputVar[] = [] + if (condition.value && typeof condition.value === 'string') { + const inputVars = getInputVars([condition.value]) + vars.push(...inputVars) + } + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getInputVarsFromCase(condition.sub_variable_condition)) + + return vars + } + + const forms = (() => { + const allInputs: ValueSelector[] = [] + const inputVarsFromValue: InputVar[] = [] + if (payload.cases && payload.cases.length) { + payload.cases.forEach((caseItem) => { + const caseVars = getVarSelectorsFromCase(caseItem) + allInputs.push(...caseVars) + inputVarsFromValue.push(...getInputVarsFromCase(caseItem)) + }) + } + + if (payload.conditions && payload.conditions.length) { + payload.conditions.forEach((condition) => { + const conditionVars = getVarSelectorsFromCondition(condition) + allInputs.push(...conditionVars) + inputVarsFromValue.push(...getInputVarsFromConditionValue(condition)) + }) + } + + const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue] + // remove duplicate inputs + const existVarsKey: Record<string, boolean> = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push(input) + } + }) + return [ + { + inputs: uniqueVarInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + const getVarFromCondition = (condition: Condition): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if(condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarFromCaseItem(condition.sub_variable_condition)) + return vars + } + + const getDependentVars = () => { + const vars: ValueSelector[] = [] + if (payload.cases && payload.cases.length) { + payload.cases.forEach((caseItem) => { + const caseVars = getVarFromCaseItem(caseItem) + vars.push(...caseVars) + }) + } + + if (payload.conditions && payload.conditions.length) { + payload.conditions.forEach((condition) => { + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index bebc140414..8458051da2 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -10,15 +10,18 @@ import { PanelComponentMap, } from './constants' import BaseNode from './_base/node' -import BasePanel from './_base/panel' +import BasePanel from './_base/components/workflow-panel' const CustomNode = (props: NodeProps) => { const nodeData = props.data - const NodeComponent = NodeComponentMap[nodeData.type] + const NodeComponent = useMemo(() => NodeComponentMap[nodeData.type], [nodeData.type]) return ( <> - <BaseNode { ...props }> + <BaseNode + id={props.id} + data={props.data} + > <NodeComponent /> </BaseNode> </> @@ -26,7 +29,12 @@ const CustomNode = (props: NodeProps) => { } CustomNode.displayName = 'CustomNode' -export const Panel = memo((props: Node) => { +export type PanelProps = { + type: Node['type'] + id: Node['id'] + data: Node['data'] +} +export const Panel = memo((props: PanelProps) => { const nodeClass = props.type const nodeData = props.data const PanelComponent = useMemo(() => { @@ -38,7 +46,11 @@ export const Panel = memo((props: Node) => { if (nodeClass === CUSTOM_NODE) { return ( - <BasePanel key={props.id} {...props}> + <BasePanel + key={props.id} + id={props.id} + data={props.data} + > <PanelComponent /> </BasePanel> ) diff --git a/web/app/components/workflow/nodes/iteration/panel.tsx b/web/app/components/workflow/nodes/iteration/panel.tsx index 1f29a07946..4b529f0785 100644 --- a/web/app/components/workflow/nodes/iteration/panel.tsx +++ b/web/app/components/workflow/nodes/iteration/panel.tsx @@ -3,20 +3,15 @@ import React from 'react' import { useTranslation } from 'react-i18next' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import Split from '../_base/components/split' -import ResultPanel from '../../run/result-panel' import { MAX_ITERATION_PARALLEL_NUM, MIN_ITERATION_PARALLEL_NUM } from '../../constants' import type { IterationNodeType } from './types' import useConfig from './use-config' -import { ErrorHandleMode, InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import { ErrorHandleMode, type NodePanelProps } from '@/app/components/workflow/types' import Field from '@/app/components/workflow/nodes/_base/components/field' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import Switch from '@/app/components/base/switch' import Select from '@/app/components/base/select' import Slider from '@/app/components/base/slider' import Input from '@/app/components/base/input' -import formatTracing from '@/app/components/workflow/run/utils/format-log' - -import { useLogs } from '@/app/components/workflow/run/hooks' const i18nPrefix = 'workflow.nodes.iteration' @@ -47,27 +42,11 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({ childrenNodeVars, iterationChildrenNodes, handleOutputVarChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - inputVarValues, - setInputVarValues, - usedOutVars, - iterator, - setIterator, - iteratorInputKey, - iterationRunResult, changeParallel, changeErrorResponseMode, changeParallelNums, } = useConfig(id, data) - const nodeInfo = formatTracing(iterationRunResult, t)[0] - const logsParams = useLogs() - return ( <div className='pb-2 pt-2'> <div className='space-y-4 px-4 pb-4'> @@ -137,38 +116,6 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({ <Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} /> </Field> </div> - - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[ - { - inputs: [...usedOutVars], - values: inputVarValues, - onChange: setInputVarValues, - }, - { - label: t(`${i18nPrefix}.input`)!, - inputs: [{ - label: '', - variable: iteratorInputKey, - type: InputVarType.iterator, - required: false, - }], - values: { [iteratorInputKey]: iterator }, - onChange: keyValue => setIterator(keyValue[iteratorInputKey]), - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - {...logsParams} - result={ - <ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} /> - } - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/iteration/types.ts b/web/app/components/workflow/nodes/iteration/types.ts index 4a20dbd456..9a68050d6f 100644 --- a/web/app/components/workflow/nodes/iteration/types.ts +++ b/web/app/components/workflow/nodes/iteration/types.ts @@ -11,6 +11,7 @@ export type IterationNodeType = CommonNodeType & { start_node_id: string // start node id in the iteration iteration_id?: string iterator_selector: ValueSelector + iterator_input_type: VarType output_selector: ValueSelector output_type: VarType // output type. is_parallel: boolean // open the parallel mode or not diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts index fd69fecaf0..c8656aa937 100644 --- a/web/app/components/workflow/nodes/iteration/use-config.ts +++ b/web/app/components/workflow/nodes/iteration/use-config.ts @@ -1,25 +1,25 @@ import { useCallback } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' import { useIsChatMode, - useIsNodeInIteration, useNodesReadOnly, useWorkflow, } from '../../hooks' import { VarType } from '../../types' import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import useNodeCrud from '../_base/hooks/use-node-crud' -import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils' -import useOneStepRun from '../_base/hooks/use-one-step-run' import type { IterationNodeType } from './types' +import { toNodeOutputVars } from '../_base/components/variable/utils' import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import type { Item } from '@/app/components/base/select' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' +import { isEqual } from 'lodash-es' -const DELIMITER = '@@@@@' const useConfig = (id: string, payload: IterationNodeType) => { + const { + deleteNodeInspectorVars, + } = useInspectVarsCrud() const { nodesReadOnly: readOnly } = useNodesReadOnly() - const { isNodeInIteration } = useIsNodeInIteration(id) const isChatMode = useIsChatMode() const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload) @@ -28,21 +28,23 @@ const useConfig = (id: string, payload: IterationNodeType) => { return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type) }, []) - const handleInputChange = useCallback((input: ValueSelector | string) => { + const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => { const newInputs = produce(inputs, (draft) => { draft.iterator_selector = input as ValueSelector || [] + draft.iterator_input_type = varInfo?.type || VarType.arrayString }) setInputs(newInputs) }, [inputs, setInputs]) // output - const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow() - const beforeNodes = getBeforeNodesInSameBranch(id) + const { getIterationNodeChildren } = useWorkflow() const iterationChildrenNodes = getIterationNodeChildren(id) - const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes] const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode) const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => { + if (isEqual(inputs.output_selector, output as ValueSelector)) + return + const newInputs = produce(inputs, (draft) => { draft.output_selector = output as ValueSelector || [] const outputItemType = varInfo?.type || VarType.string @@ -61,135 +63,8 @@ const useConfig = (id: string, payload: IterationNodeType) => { } as Record<VarType, VarType>)[outputItemType] || VarType.arrayString }) setInputs(newInputs) - }, [inputs, setInputs]) - - // single run - const iteratorInputKey = `${id}.input_selector` - const { - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun: doHandleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - iterationRunResult, - } = useOneStepRun<IterationNodeType>({ - id, - data: inputs, - iteratorInputKey, - defaultRunInputData: { - [iteratorInputKey]: [''], - }, - }) - - const [isShowIterationDetail, { - setTrue: doShowIterationDetail, - setFalse: doHideIterationDetail, - }] = useBoolean(false) - - const hideIterationDetail = useCallback(() => { - hideSingleRun() - doHideIterationDetail() - }, [doHideIterationDetail, hideSingleRun]) - - const showIterationDetail = useCallback(() => { - doShowIterationDetail() - }, [doShowIterationDetail]) - - const backToSingleRun = useCallback(() => { - hideIterationDetail() - showSingleRun() - }, [hideIterationDetail, showSingleRun]) - - const { usedOutVars, allVarObject } = (() => { - const vars: ValueSelector[] = [] - const varObjs: Record<string, boolean> = {} - const allVarObject: Record<string, { - inSingleRunPassedKey: string - }> = {} - iterationChildrenNodes.forEach((node) => { - const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) - nodeVars.forEach((varSelector) => { - if (varSelector[0] === id) { // skip iteration node itself variable: item, index - return - } - const isInIteration = isNodeInIteration(varSelector[0]) - if (isInIteration) // not pass iteration inner variable - return - - const varSectorStr = varSelector.join('.') - if (!varObjs[varSectorStr]) { - varObjs[varSectorStr] = true - vars.push(varSelector) - } - let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) - if (typeof passToServerKeys === 'string') - passToServerKeys = [passToServerKeys] - - passToServerKeys.forEach((key: string, index: number) => { - allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { - inSingleRunPassedKey: key, - } - }) - }) - }) - const res = toVarInputs(vars.map((item) => { - const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) - return { - label: { - nodeType: varInfo?.data.type, - nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title - variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], - }, - variable: `${item.join('.')}`, - value_selector: item, - } - })) - return { - usedOutVars: res, - allVarObject, - } - })() - - const handleRun = useCallback((data: Record<string, any>) => { - const formattedData: Record<string, any> = {} - Object.keys(allVarObject).forEach((key) => { - const [varSectorStr, nodeId] = key.split(DELIMITER) - formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr] - }) - formattedData[iteratorInputKey] = data[iteratorInputKey] - doHandleRun(formattedData) - }, [allVarObject, doHandleRun, iteratorInputKey]) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .filter(key => ![iteratorInputKey].includes(key)) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - const newVars = { - ...newPayload, - [iteratorInputKey]: runInputData[iteratorInputKey], - } - setRunInputData(newVars) - }, [iteratorInputKey, runInputData, setRunInputData]) - - const iterator = runInputData[iteratorInputKey] - const setIterator = useCallback((newIterator: string[]) => { - setRunInputData({ - ...runInputData, - [iteratorInputKey]: newIterator, - }) - }, [iteratorInputKey, runInputData, setRunInputData]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, id, inputs, setInputs]) const changeParallel = useCallback((value: boolean) => { const newInputs = produce(inputs, (draft) => { @@ -218,24 +93,6 @@ const useConfig = (id: string, payload: IterationNodeType) => { childrenNodeVars, iterationChildrenNodes, handleOutputVarChange, - isShowSingleRun, - showSingleRun, - hideSingleRun, - isShowIterationDetail, - showIterationDetail, - hideIterationDetail, - backToSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - inputVarValues, - setInputVarValues, - usedOutVars, - iterator, - setIterator, - iteratorInputKey, - iterationRunResult, changeParallel, changeErrorResponseMode, changeParallelNums, diff --git a/web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts b/web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts new file mode 100644 index 0000000000..b6c96bac48 --- /dev/null +++ b/web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts @@ -0,0 +1,154 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import type { IterationNodeType } from './types' +import { useTranslation } from 'react-i18next' +import { useIsNodeInIteration, useWorkflow } from '../../hooks' +import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import formatTracing from '@/app/components/workflow/run/utils/format-log' +import type { NodeTracing } from '@/types/workflow' +import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' + +const i18nPrefix = 'workflow.nodes.iteration' + +type Params = { + id: string, + payload: IterationNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + iterationRunResult: NodeTracing[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + toVarInputs, + setRunInputData, + iterationRunResult, +}: Params) => { + const { t } = useTranslation() + const { isNodeInIteration } = useIsNodeInIteration(id) + + const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow() + const iterationChildrenNodes = getIterationNodeChildren(id) + const beforeNodes = getBeforeNodesInSameBranch(id) + const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes] + + const iteratorInputKey = `${id}.input_selector` + const iterator = runInputData[iteratorInputKey] + const setIterator = useCallback((newIterator: string[]) => { + setRunInputData({ + ...runInputData, + [iteratorInputKey]: newIterator, + }) + }, [iteratorInputKey, runInputData, setRunInputData]) + + const { usedOutVars, allVarObject } = (() => { + const vars: ValueSelector[] = [] + const varObjs: Record<string, boolean> = {} + const allVarObject: Record<string, { + inSingleRunPassedKey: string + }> = {} + iterationChildrenNodes.forEach((node) => { + const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) + nodeVars.forEach((varSelector) => { + if (varSelector[0] === id) { // skip iteration node itself variable: item, index + return + } + const isInIteration = isNodeInIteration(varSelector[0]) + if (isInIteration) // not pass iteration inner variable + return + + const varSectorStr = varSelector.join('.') + if (!varObjs[varSectorStr]) { + varObjs[varSectorStr] = true + vars.push(varSelector) + } + let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) + if (typeof passToServerKeys === 'string') + passToServerKeys = [passToServerKeys] + + passToServerKeys.forEach((key: string, index: number) => { + allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { + inSingleRunPassedKey: key, + } + }) + }) + }) + const res = toVarInputs(vars.map((item) => { + const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) + return { + label: { + nodeType: varInfo?.data.type, + nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title + variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], + }, + variable: `${item.join('.')}`, + value_selector: item, + } + })) + return { + usedOutVars: res, + allVarObject, + } + })() + + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: [...usedOutVars], + values: inputVarValues, + onChange: setInputVarValues, + }, + { + label: t(`${i18nPrefix}.input`)!, + inputs: [{ + label: '', + variable: iteratorInputKey, + type: InputVarType.iterator, + required: false, + getVarValueFromDependent: true, + isFileItem: payload.iterator_input_type === VarType.arrayFile, + }], + values: { [iteratorInputKey]: iterator }, + onChange: (keyValue: Record<string, any>) => setIterator(keyValue[iteratorInputKey]), + }, + ] + }, [inputVarValues, iterator, iteratorInputKey, payload.iterator_input_type, setInputVarValues, setIterator, t, usedOutVars]) + + const nodeInfo = formatTracing(iterationRunResult, t)[0] + + const getDependentVars = () => { + return [payload.iterator_selector] + } + const getDependentVar = (variable: string) => { + if(variable === iteratorInputKey) + return payload.iterator_selector + } + + return { + forms, + nodeInfo, + allVarObject, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx index 00ba306d03..0d81d808db 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx @@ -9,7 +9,7 @@ import type { VarType } from '@/app/components/workflow/types' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' type ConditionCommonVariableSelectorProps = { - variables?: { name: string; type: string }[] + variables?: { name: string; type: string; value: string }[] value?: string | number varType?: VarType onChange: (v: string) => void @@ -24,7 +24,7 @@ const ConditionCommonVariableSelector = ({ const { t } = useTranslation() const [open, setOpen] = useState(false) - const selected = variables.find(v => v.name === value) + const selected = variables.find(v => v.value === value) const handleChange = useCallback((v: string) => { onChange(v) setOpen(false) @@ -49,7 +49,7 @@ const ConditionCommonVariableSelector = ({ selected && ( <div className='system-xs-medium inline-flex h-6 items-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark pl-[5px] pr-1.5 text-text-secondary shadow-xs'> <Variable02 className='mr-1 h-3.5 w-3.5 text-text-accent' /> - {selected.name} + {selected.value} </div> ) } @@ -73,12 +73,12 @@ const ConditionCommonVariableSelector = ({ { variables.map(v => ( <div - key={v.name} + key={v.value} className='system-xs-medium flex h-6 cursor-pointer items-center rounded-md px-2 text-text-secondary hover:bg-state-base-hover' - onClick={() => handleChange(v.name)} + onClick={() => handleChange(v.value)} > <Variable02 className='mr-1 h-4 w-4 text-text-accent' /> - {v.name} + {v.value} </div> )) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx index 3b5eefd853..267a0ef797 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx @@ -16,9 +16,7 @@ import type { KnowledgeRetrievalNodeType } from './types' import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import type { NodePanelProps } from '@/app/components/workflow/types' const i18nPrefix = 'workflow.nodes.knowledgeRetrieval' @@ -40,14 +38,6 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({ selectedDatasets, selectedDatasetsLoaded, handleOnDatasetsChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - query, - setQuery, - runResult, rerankModelOpen, setRerankModelOpen, handleAddCondition, @@ -191,28 +181,6 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({ </> </OutputVars> - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[ - { - inputs: [{ - label: t(`${i18nPrefix}.queryVariable`)!, - variable: 'query', - type: InputVarType.paragraph, - required: true, - }], - values: { query }, - onChange: keyValue => setQuery(keyValue.query), - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} </div> </div> ) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts index 42aa7def25..380f8b95c9 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts @@ -37,7 +37,6 @@ import { DATASET_DEFAULT } from '@/config' import type { DataSet } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' @@ -173,7 +172,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => { } }) setInputs(newInput) - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentProvider?.provider, currentModel, currentRerankModel, rerankDefaultModel]) const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([]) const [rerankModelOpen, setRerankModelOpen] = useState(false) @@ -230,7 +229,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => { setInputs(newInputs) setSelectedDatasetsLoaded(true) })() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { @@ -242,7 +241,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => { setInputs(produce(inputs, (draft) => { draft.query_variable_selector = query_variable_selector })) - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleOnDatasetsChange = useCallback((newDatasets: DataSet[]) => { @@ -280,32 +279,6 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => { return varPayload.type === VarType.string }, []) - // single run - const { - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun<KnowledgeRetrievalNodeType>({ - id, - data: inputs, - defaultRunInputData: { - query: '', - }, - }) - - const query = runInputData.query - const setQuery = useCallback((newQuery: string) => { - setRunInputData({ - ...runInputData, - query: newQuery, - }) - }, [runInputData, setRunInputData]) - const handleMetadataFilterModeChange = useCallback((newMode: MetadataFilteringModeEnum) => { setInputs(produce(inputRef.current, (draft) => { draft.metadata_filtering_mode = newMode @@ -425,14 +398,6 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => { selectedDatasets: selectedDatasets.filter(d => d.name), selectedDatasetsLoaded, handleOnDatasetsChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - query, - setQuery, - runResult, rerankModelOpen, setRerankModelOpen, handleMetadataFilterModeChange, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts new file mode 100644 index 0000000000..6655932790 --- /dev/null +++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts @@ -0,0 +1,63 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { InputVarType } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import type { KnowledgeRetrievalNodeType } from './types' + +const i18nPrefix = 'workflow.nodes.knowledgeRetrieval' + +type Params = { + id: string, + payload: KnowledgeRetrievalNodeType + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const query = runInputData.query + const setQuery = useCallback((newQuery: string) => { + setRunInputData({ + ...runInputData, + query: newQuery, + }) + }, [runInputData, setRunInputData]) + + const forms = useMemo(() => { + return [ + { + inputs: [{ + label: t(`${i18nPrefix}.queryVariable`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }], + values: { query }, + onChange: (keyValue: Record<string, any>) => setQuery(keyValue.query), + }, + ] + }, [query, setQuery, t]) + + const getDependentVars = () => { + return [payload.query_variable_selector] + } + const getDependentVar = (variable: string) => { + if(variable === 'query') + return payload.query_variable_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx index 2ae7fec78d..920757c6a6 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx @@ -1,4 +1,4 @@ -import React, { type FC, useCallback, useEffect, useRef } from 'react' +import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import classNames from '@/utils/classnames' @@ -14,6 +14,7 @@ type CodeEditorProps = { showFormatButton?: boolean editorWrapperClassName?: string readOnly?: boolean + hideTopMenu?: boolean } & React.HTMLAttributes<HTMLDivElement> const CodeEditor: FC<CodeEditorProps> = ({ @@ -22,12 +23,14 @@ const CodeEditor: FC<CodeEditorProps> = ({ showFormatButton = true, editorWrapperClassName, readOnly = false, + hideTopMenu = false, className, }) => { const { t } = useTranslation() const { theme } = useTheme() const monacoRef = useRef<any>(null) const editorRef = useRef<any>(null) + const [isMounted, setIsMounted] = React.useState(false) const containerRef = useRef<HTMLDivElement>(null) useEffect(() => { @@ -63,6 +66,7 @@ const CodeEditor: FC<CodeEditorProps> = ({ }, }) monaco.editor.setTheme('light-theme') + setIsMounted(true) }, []) const formatJsonContent = useCallback(() => { @@ -75,6 +79,11 @@ const CodeEditor: FC<CodeEditorProps> = ({ onUpdate?.(value) }, [onUpdate]) + const editorTheme = useMemo(() => { + if (theme === Theme.light) + return 'light-theme' + return 'dark-theme' + }, [theme]) useEffect(() => { const resizeObserver = new ResizeObserver(() => { editorRef.current?.layout() @@ -89,39 +98,39 @@ const CodeEditor: FC<CodeEditorProps> = ({ }, []) return ( - <div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', className)}> - <div className='flex items-center justify-between pl-2 pr-1 pt-1'> - <div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'> - <span className='px-1 py-0.5'>JSON</span> - </div> - <div className='flex items-center gap-x-0.5'> - {showFormatButton && ( - <Tooltip popupContent={t('common.operation.format')}> + <div className={classNames('flex h-full flex-col overflow-hidden bg-components-input-bg-normal', hideTopMenu && 'pt-2', className)}> + {!hideTopMenu && ( + <div className='flex items-center justify-between pl-2 pr-1 pt-1'> + <div className='system-xs-semibold-uppercase py-0.5 text-text-secondary'> + <span className='px-1 py-0.5'>JSON</span> + </div> + <div className='flex items-center gap-x-0.5'> + {showFormatButton && ( + <Tooltip popupContent={t('common.operation.format')}> + <button + type='button' + className='flex h-6 w-6 items-center justify-center' + onClick={formatJsonContent} + > + <RiIndentIncrease className='h-4 w-4 text-text-tertiary' /> + </button> + </Tooltip> + )} + <Tooltip popupContent={t('common.operation.copy')}> <button type='button' className='flex h-6 w-6 items-center justify-center' - onClick={formatJsonContent} - > - <RiIndentIncrease className='h-4 w-4 text-text-tertiary' /> + onClick={() => copy(value)}> + <RiClipboardLine className='h-4 w-4 text-text-tertiary' /> </button> </Tooltip> - )} - <Tooltip popupContent={t('common.operation.copy')}> - <button - type='button' - className='flex h-6 w-6 items-center justify-center' - onClick={() => copy(value)}> - <RiClipboardLine className='h-4 w-4 text-text-tertiary' /> - </button> - </Tooltip> + </div> </div> - </div> - <div - ref={containerRef} - className={classNames('relative overflow-hidden', editorWrapperClassName)} - > + )} + <div className={classNames('relative overflow-hidden', editorWrapperClassName)}> <Editor defaultLanguage='json' + theme={isMounted ? editorTheme : 'default-theme'} // sometimes not load the default theme value={value} onChange={handleEditorChange} onMount={handleEditorDidMount} diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index 2b8574b285..fecd1093d9 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -21,8 +21,8 @@ import { MittProvider, VisualEditorContextProvider, useMittContext } from './vis import ErrorMessage from './error-message' import { useVisualEditorStore } from './visual-editor/store' import Toast from '@/app/components/base/toast' -import { useGetDocLanguage } from '@/context/i18n' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' +import { useDocLink } from '@/context/i18n' type JsonSchemaConfigProps = { defaultSchema?: SchemaRoot @@ -53,7 +53,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({ onClose, }) => { const { t } = useTranslation() - const docLanguage = useGetDocLanguage() + const docLink = useDocLink() const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor) const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA) const [json, setJson] = useState(JSON.stringify(jsonSchema, null, 2)) @@ -252,7 +252,7 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({ <div className='flex items-center gap-x-2 p-6 pt-5'> <a className='flex grow items-center gap-x-1 text-text-accent' - href={`https://docs.dify.ai/${docLanguage}/guides/workflow/structured-outputs`} + href={docLink('/guides/workflow/structured-outputs')} target='_blank' rel='noopener noreferrer' > diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx index 4732499f3a..64138b3cbd 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx @@ -11,7 +11,6 @@ import { ModelModeType } from '@/types/app' import { Theme } from '@/types/app' import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets' import cn from '@/utils/classnames' -import type { ModelInfo } from './prompt-editor' import PromptEditor from './prompt-editor' import GeneratedResult from './generated-result' import { useGenerateStructuredOutputRules } from '@/service/use-common' @@ -19,7 +18,6 @@ import Toast from '@/app/components/base/toast' import { type FormValue, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useVisualEditorStore } from '../visual-editor/store' -import { useTranslation } from 'react-i18next' import { useMittContext } from '../visual-editor/context' type JsonSchemaGeneratorProps = { @@ -36,10 +34,12 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({ onApply, crossAxisOffset, }) => { - const { t } = useTranslation() + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model + : null const [open, setOpen] = useState(false) const [view, setView] = useState(GeneratorView.promptEditor) - const [model, setModel] = useState<Model>({ + const [model, setModel] = useState<Model>(localModel || { name: '', provider: '', mode: ModelModeType.completion, @@ -58,11 +58,19 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({ useEffect(() => { if (defaultModel) { - setModel(prev => ({ - ...prev, - name: defaultModel.model, - provider: defaultModel.provider.provider, - })) + const localModel = localStorage.getItem('auto-gen-model') + ? JSON.parse(localStorage.getItem('auto-gen-model') || '') + : null + if (localModel) { + setModel(localModel) + } + else { + setModel(prev => ({ + ...prev, + name: defaultModel.model, + provider: defaultModel.provider.provider, + })) + } } }, [defaultModel]) @@ -77,22 +85,25 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({ setOpen(false) }, []) - const handleModelChange = useCallback((model: ModelInfo) => { - setModel(prev => ({ - ...prev, - provider: model.provider, - name: model.modelId, - mode: model.mode as ModelModeType, - })) - }, []) + const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => { + const newModel = { + ...model, + provider: newValue.provider, + name: newValue.modelId, + mode: newValue.mode as ModelModeType, + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) const handleCompletionParamsChange = useCallback((newParams: FormValue) => { - setModel(prev => ({ - ...prev, + const newModel = { + ...model, completion_params: newParams as CompletionParams, - }), - ) - }, []) + } + setModel(newModel) + localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) + }, [model, setModel]) const { mutateAsync: generateStructuredOutputRules, isPending: isGenerating } = useGenerateStructuredOutputRules() diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx index 05a429ff72..ee3083c4e6 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx @@ -1,21 +1,30 @@ import React, { type FC } from 'react' import CodeEditor from './code-editor' +import cn from '@/utils/classnames' type SchemaEditorProps = { schema: string onUpdate: (schema: string) => void + hideTopMenu?: boolean + className?: string + readonly?: boolean } const SchemaEditor: FC<SchemaEditorProps> = ({ schema, onUpdate, + hideTopMenu, + className, + readonly = false, }) => { return ( <CodeEditor - className='grow rounded-xl' + readOnly={readonly} + className={cn('grow rounded-xl', className)} editorWrapperClassName='grow' value={schema} onUpdate={onUpdate} + hideTopMenu={hideTopMenu} /> ) } diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 470a322b13..d11ad92241 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -6,6 +6,7 @@ import type { EditData } from './edit-card' import { ArrayType, type Field, Type } from '../../../types' import Toast from '@/app/components/base/toast' import { findPropertyWithPath } from '../../../utils' +import { noop } from 'lodash-es' type ChangeEventParams = { path: string[], @@ -19,7 +20,8 @@ type AddEventParams = { } export const useSchemaNodeOperations = (props: VisualEditorProps) => { - const { schema: jsonSchema, onChange } = props + const { schema: jsonSchema, onChange: doOnChange } = props + const onChange = doOnChange || noop const backupSchema = useVisualEditorStore(state => state.backupSchema) const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema) const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx index 1df42532a6..d96f856bbb 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx @@ -2,24 +2,29 @@ import type { FC } from 'react' import type { SchemaRoot } from '../../../types' import SchemaNode from './schema-node' import { useSchemaNodeOperations } from './hooks' +import cn from '@/utils/classnames' export type VisualEditorProps = { + className?: string schema: SchemaRoot - onChange: (schema: SchemaRoot) => void + rootName?: string + readOnly?: boolean + onChange?: (schema: SchemaRoot) => void } const VisualEditor: FC<VisualEditorProps> = (props) => { - const { schema } = props + const { className, schema, readOnly } = props useSchemaNodeOperations(props) return ( - <div className='h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2'> + <div className={cn('h-full overflow-auto rounded-xl bg-background-section-burn p-1 pl-2', className)}> <SchemaNode - name='structured_output' + name={props.rootName || 'structured_output'} schema={schema} required={false} path={[]} depth={0} + readOnly={readOnly} /> </div> ) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx index 70a6b861ad..36671ab050 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx @@ -19,6 +19,7 @@ type SchemaNodeProps = { path: string[] parentPath?: string[] depth: number + readOnly?: boolean } // Support 10 levels of indentation @@ -57,6 +58,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({ path, parentPath, depth, + readOnly, }) => { const [isExpanded, setIsExpanded] = useState(true) const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty) @@ -77,11 +79,13 @@ const SchemaNode: FC<SchemaNodeProps> = ({ } const handleMouseEnter = () => { + if(readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(path.join('.')) } const handleMouseLeave = () => { + if(readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(null) } @@ -91,7 +95,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({ <div className={classNames('relative z-10', indentPadding[depth])}> {depth > 0 && hasChildren && ( <div className={classNames( - 'flex items-center absolute top-0 w-5 h-7 px-0.5 z-10 bg-background-section-burn', + 'absolute top-0 z-10 flex h-7 w-5 items-center bg-background-section-burn px-0.5', indentLeft[depth - 1], )}> <button @@ -136,8 +140,8 @@ const SchemaNode: FC<SchemaNodeProps> = ({ </div> <div className={classNames( - 'flex justify-center w-5 absolute z-0', - schema.description ? 'h-[calc(100%-3rem)] top-12' : 'h-[calc(100%-1.75rem)] top-7', + 'absolute z-0 flex w-5 justify-center', + schema.description ? 'top-12 h-[calc(100%-3rem)]' : 'top-7 h-[calc(100%-1.75rem)]', indentLeft[depth], )}> <Divider @@ -183,7 +187,7 @@ const SchemaNode: FC<SchemaNodeProps> = ({ )} { - depth === 0 && !isAddingNewField && ( + !readOnly && depth === 0 && !isAddingNewField && ( <AddField /> ) } diff --git a/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx b/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx index 804478dde3..f58a749684 100644 --- a/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx +++ b/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx @@ -9,7 +9,6 @@ import GetAutomaticResModal from '@/app/components/app/configuration/config/auto import { AppType } from '@/types/app' import type { AutomaticRes } from '@/service/debug' import type { ModelConfig } from '@/app/components/workflow/types' -import type { Model } from '@/types/app' type Props = { className?: string @@ -20,7 +19,6 @@ type Props = { const PromptGeneratorBtn: FC<Props> = ({ className, onGenerated, - modelConfig, }) => { const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const handleAutomaticRes = useCallback((res: AutomaticRes) => { @@ -40,7 +38,6 @@ const PromptGeneratorBtn: FC<Props> = ({ isShow={showAutomatic} onClose={showAutomaticFalse} onFinished={handleAutomaticRes} - model={modelConfig as Model} isInLLMNode /> )} diff --git a/web/app/components/workflow/nodes/llm/default.ts b/web/app/components/workflow/nodes/llm/default.ts index 92377f74b8..efe7fd8b64 100644 --- a/web/app/components/workflow/nodes/llm/default.ts +++ b/web/app/components/workflow/nodes/llm/default.ts @@ -1,8 +1,29 @@ +// import { RETRIEVAL_OUTPUT_STRUCT } from '../../constants' import { BlockEnum, EditionType } from '../../types' import { type NodeDefault, type PromptItem, PromptRole } from '../../types' import type { LLMNodeType } from './types' import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' +const RETRIEVAL_OUTPUT_STRUCT = `{ + "content": "", + "title": "", + "url": "", + "icon": "", + "metadata": { + "dataset_id": "", + "dataset_name": "", + "document_id": [], + "document_name": "", + "document_data_source_type": "", + "segment_id": "", + "segment_position": "", + "segment_word_count": "", + "segment_hit_count": "", + "segment_index_node_hash": "", + "score": "" + } +}` + const i18nPrefix = 'workflow.errorMsg' const nodeDefault: NodeDefault<LLMNodeType> = { @@ -27,6 +48,10 @@ const nodeDefault: NodeDefault<LLMNodeType> = { enabled: false, }, }, + defaultRunInputData: { + '#context#': [RETRIEVAL_OUTPUT_STRUCT], + '#files#': [], + }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 29fb4fb2c3..471d65ef20 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -5,7 +5,6 @@ import MemoryConfig from '../_base/components/memory-config' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import ConfigVision from '../_base/components/config-vision' import useConfig from './use-config' -import { findVariableWhenOnLLMVision } from '../utils' import type { LLMNodeType } from './types' import ConfigPrompt from './components/config-prompt' import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list' @@ -14,15 +13,14 @@ import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import type { NodePanelProps } from '@/app/components/workflow/types' import Tooltip from '@/app/components/base/tooltip' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' import StructureOutput from './components/structure-output' import Switch from '@/app/components/base/switch' import { RiAlertFill, RiQuestionLine } from '@remixicon/react' +import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' +import Toast from '@/app/components/base/toast' const i18nPrefix = 'workflow.nodes.llm' @@ -31,7 +29,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ data, }) => { const { t } = useTranslation() - const { readOnly, inputs, @@ -58,89 +55,42 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ handleMemoryChange, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - inputVarValues, - setInputVarValues, - visionFiles, - setVisionFiles, - contexts, - setContexts, - runningStatus, isModelSupportStructuredOutput, structuredOutputCollapsed, setStructuredOutputCollapsed, handleStructureOutputEnableChange, handleStructureOutputChange, - handleRun, - handleStop, - varInputs, - runResult, filterJinjia2InputVar, } = useConfig(id, data) const model = inputs.model - const singleRunForms = (() => { - const forms: FormProps[] = [] - - if (varInputs.length > 0) { - forms.push( - { - label: t(`${i18nPrefix}.singleRun.variable`)!, - inputs: varInputs, - values: inputVarValues, - onChange: setInputVarValues, - }, - ) - } - - if (inputs.context?.variable_selector && inputs.context?.variable_selector.length > 0) { - forms.push( - { - label: t(`${i18nPrefix}.context`)!, - inputs: [{ - label: '', - variable: '#context#', - type: InputVarType.contexts, - required: false, - }], - values: { '#context#': contexts }, - onChange: keyValue => setContexts(keyValue['#context#']), - }, - ) - } - - if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { - const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVars) - - forms.push( - { - label: t(`${i18nPrefix}.vision`)!, - inputs: [{ - label: currentVariable?.variable as any, - variable: '#files#', - type: currentVariable?.formType as any, - required: false, - }], - values: { '#files#': visionFiles }, - onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), - }, - ) - } - - return forms - })() - const handleModelChange = useCallback((model: { provider: string modelId: string mode?: string }) => { - handleCompletionParamsChange({}) - handleModelChanged(model) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + (async () => { + try { + const { params: filtered, removedDetails } = await fetchAndMergeValidCompletionParams( + model.provider, + model.modelId, + inputs.model.completion_params, + ) + const keys = Object.keys(removedDetails) + if (keys.length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}` }) + handleCompletionParamsChange(filtered) + } + catch (e) { + Toast.notify({ type: 'error', message: t('common.error') }) + handleCompletionParamsChange({}) + } + finally { + handleModelChanged(model) + } + })() + }, [inputs.model.completion_params]) return ( <div className='mt-2'> @@ -332,6 +282,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ type='string' description={t(`${i18nPrefix}.outputVars.output`)} /> + <VarItem + name='usage' + type='object' + description={t(`${i18nPrefix}.outputVars.usage`)} + /> {inputs.structured_output_enabled && ( <> <Split className='mt-3' /> @@ -344,18 +299,6 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ )} </> </OutputVars> - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - nodeType={inputs.type} - onHide={hideSingleRun} - forms={singleRunForms} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/llm/use-config.ts b/web/app/components/workflow/nodes/llm/use-config.ts index 13db9e4031..e4da65bedc 100644 --- a/web/app/components/workflow/nodes/llm/use-config.ts +++ b/web/app/components/workflow/nodes/llm/use-config.ts @@ -16,9 +16,8 @@ import { ModelTypeEnum, } from '@/app/components/header/account-setting/model-provider-page/declarations' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' -import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants' import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' +import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: LLMNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -29,6 +28,8 @@ const useConfig = (id: string, payload: LLMNodeType) => { const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload) const inputRef = useRef(inputs) + const { deleteNodeInspectorVars } = useInspectVarsCrud() + const setInputs = useCallback((newInputs: LLMNodeType) => { if (newInputs.memory && !newInputs.memory.role_prefix) { const newPayload = produce(newInputs, (draft) => { @@ -247,11 +248,11 @@ const useConfig = (id: string, payload: LLMNodeType) => { }, [inputs, setInputs]) const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => { - const newInputs = produce(inputRef.current, (draft) => { + const newInputs = produce(inputs, (draft) => { draft.prompt_template = newPrompt }) setInputs(newInputs) - }, [setInputs]) + }, [inputs, setInputs]) const handleMemoryChange = useCallback((newMemory?: Memory) => { const newInputs = produce(inputs, (draft) => { @@ -293,14 +294,16 @@ const useConfig = (id: string, payload: LLMNodeType) => { setInputs(newInputs) if (enabled) setStructuredOutputCollapsed(false) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [inputs, setInputs, deleteNodeInspectorVars, id]) const handleStructureOutputChange = useCallback((newOutput: StructuredOutput) => { const newInputs = produce(inputs, (draft) => { draft.structured_output = newOutput }) setInputs(newInputs) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [inputs, setInputs, deleteNodeInspectorVars, id]) const filterInputVar = useCallback((varPayload: Var) => { return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type) @@ -322,81 +325,6 @@ const useConfig = (id: string, payload: LLMNodeType) => { filterVar: filterMemoryPromptVar, }) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - runInputDataRef, - setRunInputData, - runResult, - toVarInputs, - } = useOneStepRun<LLMNodeType>({ - id, - data: inputs, - defaultRunInputData: { - '#context#': [RETRIEVAL_OUTPUT_STRUCT], - '#files#': [], - }, - }) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .filter(key => !['#context#', '#files#'].includes(key)) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - const newVars = { - ...newPayload, - '#context#': runInputDataRef.current['#context#'], - '#files#': runInputDataRef.current['#files#'], - } - setRunInputData(newVars) - }, [runInputDataRef, setRunInputData]) - - const contexts = runInputData['#context#'] - const setContexts = useCallback((newContexts: string[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#context#': newContexts, - }) - }, [runInputDataRef, setRunInputData]) - - const visionFiles = runInputData['#files#'] - const setVisionFiles = useCallback((newFiles: any[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#files#': newFiles, - }) - }, [runInputDataRef, setRunInputData]) - - const allVarStrArr = (() => { - const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text] - if (isChatMode && isChatModel && !!inputs.memory) { - arr.push('{{#sys.query#}}') - arr.push(inputs.memory.query_prompt_template) - } - - return arr - })() - - const varInputs = (() => { - const vars = getInputVars(allVarStrArr) - if (isShowVars) - return [...vars, ...toVarInputs(inputs.prompt_config?.jinja2_variables || [])] - - return vars - })() - return { readOnly, isChatMode, @@ -423,24 +351,11 @@ const useConfig = (id: string, payload: LLMNodeType) => { handleSyeQueryChange, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - inputVarValues, - setInputVarValues, - visionFiles, - setVisionFiles, - contexts, - setContexts, - varInputs, - runningStatus, isModelSupportStructuredOutput, handleStructureOutputChange, structuredOutputCollapsed, setStructuredOutputCollapsed, handleStructureOutputEnableChange, - handleRun, - handleStop, - runResult, filterJinjia2InputVar, } } diff --git a/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts new file mode 100644 index 0000000000..93a8638d05 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts @@ -0,0 +1,198 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { InputVar, PromptItem, Var, Variable } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import type { LLMNodeType } from './types' +import { EditionType } from '../../types' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { useIsChatMode } from '../../hooks' +import { useCallback } from 'react' +import useConfigVision from '../../hooks/use-config-vision' +import { noop } from 'lodash-es' +import { findVariableWhenOnLLMVision } from '../utils' +import useAvailableVarList from '../_base/hooks/use-available-var-list' + +const i18nPrefix = 'workflow.nodes.llm' +type Params = { + id: string, + payload: LLMNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, + toVarInputs, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud<LLMNodeType>(id, payload) + const getVarInputs = getInputVars + const isChatMode = useIsChatMode() + + const contexts = runInputData['#context#'] + const setContexts = useCallback((newContexts: string[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#context#': newContexts, + }) + }, [runInputDataRef, setRunInputData]) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + // model + const model = inputs.model + const modelMode = inputs.model?.mode + const isChatModel = modelMode === 'chat' + const { + isVisionModel, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: noop, + }) + + const isShowVars = (() => { + if (isChatModel) + return (inputs.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2) + + return (inputs.prompt_template as PromptItem).edition_type === EditionType.jinja2 + })() + + const filterMemoryPromptVar = useCallback((varPayload: Var) => { + return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + + const { + availableVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterMemoryPromptVar, + }) + + const allVarStrArr = (() => { + const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text] + if (isChatMode && isChatModel && !!inputs.memory) { + arr.push('{{#sys.query#}}') + arr.push(inputs.memory.query_prompt_template) + } + + return arr + })() + const varInputs = (() => { + const vars = getVarInputs(allVarStrArr) || [] + if (isShowVars) + return [...vars, ...(toVarInputs ? (toVarInputs(inputs.prompt_config?.jinja2_variables || [])) : [])] + + return vars + })() + + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .filter(key => !['#context#', '#files#'].includes(key)) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + const newVars = { + ...newPayload, + '#context#': runInputDataRef.current['#context#'], + '#files#': runInputDataRef.current['#files#'], + } + setRunInputData?.(newVars) + }, [runInputDataRef, setRunInputData]) + + const forms = (() => { + const forms: FormProps[] = [] + + if (varInputs.length > 0) { + forms.push( + { + label: t(`${i18nPrefix}.singleRun.variable`)!, + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + } + + if (inputs.context?.variable_selector && inputs.context?.variable_selector.length > 0) { + forms.push( + { + label: t(`${i18nPrefix}.context`)!, + inputs: [{ + label: '', + variable: '#context#', + type: InputVarType.contexts, + required: false, + }], + values: { '#context#': contexts }, + onChange: keyValue => setContexts(keyValue['#context#']), + }, + ) + } + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVars) + + forms.push( + { + label: t(`${i18nPrefix}.vision`)!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: { '#files#': visionFiles }, + onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), + }, + ) + } + return forms + })() + + const getDependentVars = () => { + const promptVars = varInputs.map(item => item.variable.slice(1, -1).split('.')) + const contextVar = payload.context.variable_selector + const vars = [...promptVars, contextVar] + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const visionVar = payload.vision.configs.variable_selector + vars.push(visionVar) + } + return vars + } + + const getDependentVar = (variable: string) => { + if(variable === '#context#') + return payload.context.variable_selector + + if(variable === '#files#') + return payload.vision.configs?.variable_selector + + return false + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx index d33e9361ad..0e8650d743 100644 --- a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx @@ -10,6 +10,8 @@ import type { LoopVariable, LoopVariablesComponentShape, } from '@/app/components/workflow/nodes/loop/types' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' +import Toast from '@/app/components/base/toast' type ItemProps = { item: LoopVariable @@ -21,7 +23,22 @@ const Item = ({ handleUpdateLoopVariable, }: ItemProps) => { const { t } = useTranslation() + + const checkVariableName = (value: string) => { + const { isValid, errorMessageKey } = checkKeys([value], false) + if (!isValid) { + Toast.notify({ + type: 'error', + message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }), + }) + return false + } + return true + } const handleUpdateItemLabel = useCallback((e: any) => { + replaceSpaceWithUnderscreInVarNameInput(e.target) + if (!!e.target.value && !checkVariableName(e.target.value)) + return handleUpdateLoopVariable(item.id, { label: e.target.value }) }, [item.id, handleUpdateLoopVariable]) @@ -44,6 +61,7 @@ const Item = ({ <Input value={item.label} onChange={handleUpdateItemLabel} + onBlur={e => checkVariableName(e.target.value)} autoFocus={!item.label} placeholder={t('workflow.nodes.loop.variableName')} /> diff --git a/web/app/components/workflow/nodes/loop/panel.tsx b/web/app/components/workflow/nodes/loop/panel.tsx index 8114160efd..d3f06482c2 100644 --- a/web/app/components/workflow/nodes/loop/panel.tsx +++ b/web/app/components/workflow/nodes/loop/panel.tsx @@ -1,9 +1,8 @@ import type { FC } from 'react' -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import { RiAddLine } from '@remixicon/react' import Split from '../_base/components/split' -import ResultPanel from '../../run/result-panel' import InputNumberWithSlider from '../_base/components/input-number-with-slider' import type { LoopNodeType } from './types' import useConfig from './use-config' @@ -11,10 +10,7 @@ import ConditionWrap from './components/condition-wrap' import LoopVariable from './components/loop-variables' import type { NodePanelProps } from '@/app/components/workflow/types' import Field from '@/app/components/workflow/nodes/_base/components/field' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import formatTracing from '@/app/components/workflow/run/utils/format-log' -import { useLogs } from '@/app/components/workflow/run/hooks' import { LOOP_NODE_MAX_COUNT } from '@/config' const i18nPrefix = 'workflow.nodes.loop' @@ -30,13 +26,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({ inputs, childrenNodeVars, loopChildrenNodes, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - loopRunResult, handleAddCondition, handleUpdateCondition, handleRemoveCondition, @@ -51,23 +40,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({ handleUpdateLoopVariable, } = useConfig(id, data) - const nodeInfo = useMemo(() => { - const formattedNodeInfo = formatTracing(loopRunResult, t)[0] - - if (runResult && formattedNodeInfo) { - return { - ...formattedNodeInfo, - execution_metadata: { - ...runResult.execution_metadata, - ...formattedNodeInfo.execution_metadata, - }, - } - } - - return formattedNodeInfo - }, [runResult, loopRunResult, t]) - const logsParams = useLogs() - return ( <div className='mt-2'> <div> @@ -139,20 +111,6 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({ </Select> </Field> </div> */} - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - {...logsParams} - result={ - <ResultPanel {...runResult} showSteps={false} nodeInfo={nodeInfo} {...logsParams} /> - } - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts index fbd350c229..965fe2b395 100644 --- a/web/app/components/workflow/nodes/loop/use-config.ts +++ b/web/app/components/workflow/nodes/loop/use-config.ts @@ -3,7 +3,6 @@ import { useRef, } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' import { v4 as uuid4 } from 'uuid' import { useIsChatMode, @@ -12,10 +11,9 @@ import { useWorkflow, } from '../../hooks' import { ValueType, VarType } from '../../types' -import type { ErrorHandleMode, ValueSelector, Var } from '../../types' +import type { ErrorHandleMode, Var } from '../../types' import useNodeCrud from '../_base/hooks/use-node-crud' -import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils' -import useOneStepRun from '../_base/hooks/use-one-step-run' +import { toNodeOutputVars } from '../_base/components/variable/utils' import { getOperators } from './utils' import { LogicalOperator } from './types' import type { HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, LoopNodeType } from './types' @@ -47,140 +45,12 @@ const useConfig = (id: string, payload: LoopNodeType) => { const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables) - // single run - const loopInputKey = `${id}.input_selector` - const { - isShowSingleRun, - showSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun: doHandleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - loopRunResult, - } = useOneStepRun<LoopNodeType>({ - id, - data: inputs, - loopInputKey, - defaultRunInputData: { - [loopInputKey]: [''], - }, - }) - - const [isShowLoopDetail, { - setTrue: doShowLoopDetail, - setFalse: doHideLoopDetail, - }] = useBoolean(false) - - const hideLoopDetail = useCallback(() => { - hideSingleRun() - doHideLoopDetail() - }, [doHideLoopDetail, hideSingleRun]) - - const showLoopDetail = useCallback(() => { - doShowLoopDetail() - }, [doShowLoopDetail]) - - const backToSingleRun = useCallback(() => { - hideLoopDetail() - showSingleRun() - }, [hideLoopDetail, showSingleRun]) - const { getIsVarFileAttribute, } = useIsVarFileAttribute({ nodeId: id, }) - const { usedOutVars, allVarObject } = (() => { - const vars: ValueSelector[] = [] - const varObjs: Record<string, boolean> = {} - const allVarObject: Record<string, { - inSingleRunPassedKey: string - }> = {} - loopChildrenNodes.forEach((node) => { - const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) - nodeVars.forEach((varSelector) => { - if (varSelector[0] === id) { // skip Loop node itself variable: item, index - return - } - const isInLoop = isNodeInLoop(varSelector[0]) - if (isInLoop) // not pass loop inner variable - return - - const varSectorStr = varSelector.join('.') - if (!varObjs[varSectorStr]) { - varObjs[varSectorStr] = true - vars.push(varSelector) - } - let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) - if (typeof passToServerKeys === 'string') - passToServerKeys = [passToServerKeys] - - passToServerKeys.forEach((key: string, index: number) => { - allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { - inSingleRunPassedKey: key, - } - }) - }) - }) - const res = toVarInputs(vars.map((item) => { - const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) - return { - label: { - nodeType: varInfo?.data.type, - nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title - variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], - }, - variable: `${item.join('.')}`, - value_selector: item, - } - })) - return { - usedOutVars: res, - allVarObject, - } - })() - - const handleRun = useCallback((data: Record<string, any>) => { - const formattedData: Record<string, any> = {} - Object.keys(allVarObject).forEach((key) => { - const [varSectorStr, nodeId] = key.split(DELIMITER) - formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr] - }) - formattedData[loopInputKey] = data[loopInputKey] - doHandleRun(formattedData) - }, [allVarObject, doHandleRun, loopInputKey]) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .filter(key => ![loopInputKey].includes(key)) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - const newVars = { - ...newPayload, - [loopInputKey]: runInputData[loopInputKey], - } - setRunInputData(newVars) - }, [loopInputKey, runInputData, setRunInputData]) - - const loop = runInputData[loopInputKey] - const setLoop = useCallback((newLoop: string[]) => { - setRunInputData({ - ...runInputData, - [loopInputKey]: newLoop, - }) - }, [loopInputKey, runInputData, setRunInputData]) - const changeErrorResponseMode = useCallback((item: { value: unknown }) => { const newInputs = produce(inputs, (draft) => { draft.error_handle_mode = item.value as ErrorHandleMode @@ -342,24 +212,6 @@ const useConfig = (id: string, payload: LoopNodeType) => { filterInputVar, childrenNodeVars, loopChildrenNodes, - isShowSingleRun, - showSingleRun, - hideSingleRun, - isShowLoopDetail, - showLoopDetail, - hideLoopDetail, - backToSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - inputVarValues, - setInputVarValues, - usedOutVars, - loop, - setLoop, - loopInputKey, - loopRunResult, handleAddCondition, handleRemoveCondition, handleUpdateCondition, diff --git a/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts b/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts new file mode 100644 index 0000000000..394ab9b16f --- /dev/null +++ b/web/app/components/workflow/nodes/loop/use-single-run-form-params.ts @@ -0,0 +1,221 @@ +import type { NodeTracing } from '@/types/workflow' +import { useCallback, useMemo } from 'react' +import formatTracing from '@/app/components/workflow/run/utils/format-log' +import { useTranslation } from 'react-i18next' +import { useIsNodeInLoop, useWorkflow } from '../../hooks' +import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils' +import type { InputVar, ValueSelector, Variable } from '../../types' +import type { CaseItem, Condition, LoopNodeType } from './types' +import { ValueType } from '@/app/components/workflow/types' +import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config' + +type Params = { + id: string + payload: LoopNodeType + runInputData: Record<string, any> + runResult: NodeTracing + loopRunResult: NodeTracing[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} + +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runResult, + loopRunResult, + setRunInputData, + toVarInputs, + varSelectorsToVarInputs, +}: Params) => { + const { t } = useTranslation() + + const { isNodeInLoop } = useIsNodeInLoop(id) + + const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow() + const loopChildrenNodes = getLoopNodeChildren(id) + const beforeNodes = getBeforeNodesInSameBranch(id) + const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] + + const { usedOutVars, allVarObject } = (() => { + const vars: ValueSelector[] = [] + const varObjs: Record<string, boolean> = {} + const allVarObject: Record<string, { + inSingleRunPassedKey: string + }> = {} + loopChildrenNodes.forEach((node) => { + const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0) + nodeVars.forEach((varSelector) => { + if (varSelector[0] === id) { // skip loop node itself variable: item, index + return + } + const isInLoop = isNodeInLoop(varSelector[0]) + if (isInLoop) // not pass loop inner variable + return + + const varSectorStr = varSelector.join('.') + if (!varObjs[varSectorStr]) { + varObjs[varSectorStr] = true + vars.push(varSelector) + } + let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector) + if (typeof passToServerKeys === 'string') + passToServerKeys = [passToServerKeys] + + passToServerKeys.forEach((key: string, index: number) => { + allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = { + inSingleRunPassedKey: key, + } + }) + }) + }) + + const res = toVarInputs(vars.map((item) => { + const varInfo = getNodeInfoById(canChooseVarNodes, item[0]) + return { + label: { + nodeType: varInfo?.data.type, + nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title + variable: isSystemVar(item) ? item.join('.') : item[item.length - 1], + }, + variable: `${item.join('.')}`, + value_selector: item, + } + })) + return { + usedOutVars: res, + allVarObject, + } + })() + + const nodeInfo = useMemo(() => { + const formattedNodeInfo = formatTracing(loopRunResult, t)[0] + + if (runResult && formattedNodeInfo) { + return { + ...formattedNodeInfo, + execution_metadata: { + ...runResult.execution_metadata, + ...formattedNodeInfo.execution_metadata, + }, + } + } + + return formattedNodeInfo + }, [runResult, loopRunResult, t]) + + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const getVarSelectorsFromCase = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarSelectorsFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarSelectorsFromCondition = (condition: Condition) => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if (condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarSelectorsFromCase(condition.sub_variable_condition)) + return vars + } + + const forms = (() => { + const allInputs: ValueSelector[] = [] + payload.break_conditions?.forEach((condition) => { + const vars = getVarSelectorsFromCondition(condition) + allInputs.push(...vars) + }) + + payload.loop_variables?.forEach((loopVariable) => { + if(loopVariable.value_type === ValueType.variable) + allInputs.push(loopVariable.value) + }) + const inputVarsFromValue: InputVar[] = [] + const varInputs = [...varSelectorsToVarInputs(allInputs), ...inputVarsFromValue] + + const existVarsKey: Record<string, boolean> = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push(input) + } + }) + return [ + { + inputs: [...usedOutVars, ...uniqueVarInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getVarFromCaseItem = (caseItem: CaseItem): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (caseItem.conditions && caseItem.conditions.length) { + caseItem.conditions.forEach((condition) => { + // eslint-disable-next-line ts/no-use-before-define + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + } + return vars + } + + const getVarFromCondition = (condition: Condition): ValueSelector[] => { + const vars: ValueSelector[] = [] + if (condition.variable_selector) + vars.push(condition.variable_selector) + + if(condition.sub_variable_condition && condition.sub_variable_condition.conditions?.length) + vars.push(...getVarFromCaseItem(condition.sub_variable_condition)) + return vars + } + + const getDependentVars = () => { + const vars: ValueSelector[] = usedOutVars.map(item => item.variable.split('.')) + payload.break_conditions?.forEach((condition) => { + const conditionVars = getVarFromCondition(condition) + vars.push(...conditionVars) + }) + payload.loop_variables?.forEach((loopVariable) => { + if(loopVariable.value_type === ValueType.variable) + vars.push(loopVariable.value) + }) + const hasFilterLoopVars = vars.filter(item => item[0] !== id) + return hasFilterLoopVars + } + + return { + forms, + nodeInfo, + allVarObject, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx index d03f1d9ff3..a169217609 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/panel.tsx @@ -4,9 +4,7 @@ import { useTranslation } from 'react-i18next' import MemoryConfig from '../_base/components/memory-config' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import Editor from '../_base/components/prompt/editor' -import ResultPanel from '../../run/result-panel' import ConfigVision from '../_base/components/config-vision' -import { findVariableWhenOnLLMVision } from '../utils' import useConfig from './use-config' import type { ParameterExtractorNodeType } from './types' import ExtractParameter from './components/extract-parameter/list' @@ -17,12 +15,10 @@ import Field from '@/app/components/workflow/nodes/_base/components/field' import Split from '@/app/components/workflow/nodes/_base/components/split' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' +import type { NodePanelProps } from '@/app/components/workflow/types' import Tooltip from '@/app/components/base/tooltip' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import { VarType } from '@/app/components/workflow/types' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' const i18nPrefix = 'workflow.nodes.parameterExtractor' const i18nCommonPrefix = 'workflow.common' @@ -53,63 +49,13 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({ handleReasoningModeChange, availableVars, availableNodesWithParent, - availableVisionVars, - inputVarValues, - varInputs, isVisionModel, handleVisionResolutionChange, handleVisionResolutionEnabledChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - setInputVarValues, - visionFiles, - setVisionFiles, } = useConfig(id, data) const model = inputs.model - const singleRunForms = (() => { - const forms: FormProps[] = [] - - forms.push( - { - label: t('workflow.nodes.llm.singleRun.variable')!, - inputs: [{ - label: t(`${i18nPrefix}.inputVar`)!, - variable: 'query', - type: InputVarType.paragraph, - required: true, - }, ...varInputs], - values: inputVarValues, - onChange: setInputVarValues, - }, - ) - - if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { - const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVisionVars) - - forms.push( - { - label: t('workflow.nodes.llm.vision')!, - inputs: [{ - label: currentVariable?.variable as any, - variable: '#files#', - type: currentVariable?.formType as any, - required: false, - }], - values: { '#files#': visionFiles }, - onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), - }, - ) - } - - return forms - })() - return ( <div className='pt-2'> <div className='space-y-4 px-4'> @@ -244,28 +190,22 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({ <VarItem name='__is_success' type={VarType.number} - description={t(`${i18nPrefix}.isSuccess`)} + description={t(`${i18nPrefix}.outputVars.isSuccess`)} /> <VarItem name='__reason' type={VarType.string} - description={t(`${i18nPrefix}.errorReason`)} + description={t(`${i18nPrefix}.outputVars.errorReason`)} + /> + <VarItem + name='__usage' + type='object' + description={t(`${i18nPrefix}.outputVars.usage`)} /> </> </OutputVars> </div> </>)} - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={singleRunForms} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts index 045737b230..3fe42b60cf 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts @@ -8,7 +8,6 @@ import { useNodesReadOnly, useWorkflow, } from '../../hooks' -import useOneStepRun from '../_base/hooks/use-one-step-run' import useConfigVision from '../../hooks/use-config-vision' import type { Param, ParameterExtractorNodeType, ReasoningModeType } from './types' import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -17,8 +16,13 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { supportFunctionCall } from '@/utils/tool-call' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: ParameterExtractorNodeType) => { + const { + deleteNodeInspectorVars, + renameInspectVarName, + } = useInspectVarsCrud() const { nodesReadOnly: readOnly } = useNodesReadOnly() const { handleOutVarRenameChange } = useWorkflow() const isChatMode = useIsChatMode() @@ -59,9 +63,14 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { }) setInputs(newInputs) - if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload) + if (moreInfo && moreInfo?.type === ChangeType.changeVarName && moreInfo.payload) { handleOutVarRenameChange(id, [id, moreInfo.payload.beforeKey], [id, moreInfo.payload.afterKey!]) - }, [handleOutVarRenameChange, id, inputs, setInputs]) + renameInspectVarName(id, moreInfo.payload.beforeKey, moreInfo.payload.afterKey!) + } + else { + deleteNodeInspectorVars(id) + } + }, [deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, renameInspectVarName, setInputs]) const addExtractParameter = useCallback((payload: Param) => { const newInputs = produce(inputs, (draft) => { @@ -70,7 +79,8 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { draft.parameters.push(payload) }) setInputs(newInputs) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, id, inputs, setInputs]) // model const model = inputs.model || { @@ -145,7 +155,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { return setModelChanged(false) handleVisionConfigAfterModelChanged() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisionModel, modelChanged]) const { @@ -163,10 +173,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { return [VarType.number, VarType.string].includes(varPayload.type) }, []) - const filterVisionInputVar = useCallback((varPayload: Var) => { - return [VarType.file, VarType.arrayFile].includes(varPayload.type) - }, []) - const { availableVars, availableNodesWithParent, @@ -175,13 +181,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { filterVar: filterInputVar, }) - const { - availableVars: availableVisionVars, - } = useAvailableVarList(id, { - onlyLeafNodeVar: false, - filterVar: filterVisionInputVar, - }) - const handleCompletionParamsChange = useCallback((newParams: Record<string, any>) => { const newInputs = produce(inputs, (draft) => { draft.model.completion_params = newParams @@ -223,49 +222,6 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - runInputDataRef, - setRunInputData, - runResult, - } = useOneStepRun<ParameterExtractorNodeType>({ - id, - data: inputs, - defaultRunInputData: { - 'query': '', - '#files#': [], - }, - }) - - const varInputs = getInputVars([inputs.instruction]) - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - setRunInputData(newPayload) - }, [setRunInputData]) - - const visionFiles = runInputData['#files#'] - const setVisionFiles = useCallback((newFiles: any[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#files#': newFiles, - }) - }, [runInputDataRef, setRunInputData]) - return { readOnly, handleInputVarChange, @@ -283,24 +239,12 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { hasSetBlockStatus, availableVars, availableNodesWithParent, - availableVisionVars, isSupportFunctionCall, handleReasoningModeChange, handleMemoryChange, - varInputs, - inputVarValues, isVisionModel, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, - setInputVarValues, - visionFiles, - setVisionFiles, } } diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts new file mode 100644 index 0000000000..178f9e3ed8 --- /dev/null +++ b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts @@ -0,0 +1,148 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { InputVar, Var, Variable } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import type { ParameterExtractorNodeType } from './types' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { useCallback } from 'react' +import useConfigVision from '../../hooks/use-config-vision' +import { noop } from 'lodash-es' +import { findVariableWhenOnLLMVision } from '../utils' +import useAvailableVarList from '../_base/hooks/use-available-var-list' + +const i18nPrefix = 'workflow.nodes.parameterExtractor' + +type Params = { + id: string, + payload: ParameterExtractorNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud<ParameterExtractorNodeType>(id, payload) + + const model = inputs.model + + const { + isVisionModel, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: noop, + }) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + const varInputs = getInputVars([inputs.instruction]) + + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .filter(key => !['#context#', '#files#'].includes(key)) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + const newVars = { + ...newPayload, + '#context#': runInputDataRef.current['#context#'], + '#files#': runInputDataRef.current['#files#'], + } + setRunInputData?.(newVars) + }, [runInputDataRef, setRunInputData]) + + const filterVisionInputVar = useCallback((varPayload: Var) => { + return [VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + const { + availableVars: availableVisionVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterVisionInputVar, + }) + + const forms = (() => { + const forms: FormProps[] = [] + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: [{ + label: t(`${i18nPrefix}.inputVar`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }, ...varInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVisionVars) + + forms.push( + { + label: t('workflow.nodes.llm.vision')!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: { '#files#': visionFiles }, + onChange: keyValue => setVisionFiles((keyValue as any)['#files#']), + }, + ) + } + + return forms + })() + + const getDependentVars = () => { + const promptVars = varInputs.map(item => item.variable.slice(1, -1).split('.')) + const vars = [payload.query, ...promptVars] + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const visionVar = payload.vision.configs.variable_selector + vars.push(visionVar) + } + return vars + } + + const getDependentVar = (variable: string) => { + if(variable === 'query') + return payload.query + if(variable === '#files#') + return payload.vision.configs?.variable_selector + + return false + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx index 6065037322..478ac925d6 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx @@ -1,15 +1,18 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import type { Topic } from '../types' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { uniqueId } from 'lodash-es' const i18nPrefix = 'workflow.nodes.questionClassifiers' type Props = { + className?: string + headerClassName?: string nodeId: string payload: Topic onChange: (payload: Topic) => void @@ -20,6 +23,8 @@ type Props = { } const ClassItem: FC<Props> = ({ + className, + headerClassName, nodeId, payload, onChange, @@ -29,6 +34,11 @@ const ClassItem: FC<Props> = ({ filterVar, }) => { const { t } = useTranslation() + const [instanceId, setInstanceId] = useState(uniqueId()) + + useEffect(() => { + setInstanceId(`${nodeId}-${uniqueId()}`) + }, [nodeId]) const handleNameChange = useCallback((value: string) => { onChange({ ...payload, name: value }) @@ -43,6 +53,8 @@ const ClassItem: FC<Props> = ({ return ( <Editor + className={className} + headerClassName={headerClassName} title={`${t(`${i18nPrefix}.class`)} ${index}`} placeholder={t(`${i18nPrefix}.topicPlaceholder`)!} value={payload.name} @@ -52,6 +64,7 @@ const ClassItem: FC<Props> = ({ nodesOutputVars={availableVars} availableNodes={availableNodesWithParent} readOnly={readonly} // ? + instanceId={instanceId} justVar // ? isSupportFileVar // ? /> diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index f152917ed4..d0297cfd74 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -8,6 +8,9 @@ import AddButton from '../../_base/components/add-button' import Item from './class-item' import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { ReactSortable } from 'react-sortablejs' +import { noop } from 'lodash-es' +import cn from '@/utils/classnames' const i18nPrefix = 'workflow.nodes.questionClassifiers' @@ -17,6 +20,7 @@ type Props = { onChange: (list: Topic[]) => void readonly?: boolean filterVar: (payload: Var, valueSelector: ValueSelector) => boolean + handleSortTopic?: (newTopics: (Topic & { id: string })[]) => void } const ClassList: FC<Props> = ({ @@ -25,6 +29,7 @@ const ClassList: FC<Props> = ({ onChange, readonly, filterVar, + handleSortTopic = noop, }) => { const { t } = useTranslation() const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() @@ -55,22 +60,48 @@ const ClassList: FC<Props> = ({ } }, [list, onChange, handleEdgeDeleteByDeleteBranch, nodeId]) + const topicCount = list.length + const handleSideWidth = 3 // Todo Remove; edit topic name return ( - <div className='space-y-2'> + <ReactSortable + list={list.map(item => ({ ...item }))} + setList={handleSortTopic} + handle='.handle' + ghostClass='bg-components-panel-bg' + animation={150} + disabled={readonly} + className='space-y-2' + > { list.map((item, index) => { + const canDrag = (() => { + if (readonly) + return false + + return topicCount >= 2 + })() return ( - <Item - nodeId={nodeId} - key={list[index].id} - payload={item} - onChange={handleClassChange(index)} - onRemove={handleRemoveClass(index)} - index={index + 1} - readonly={readonly} - filterVar={filterVar} - /> + <div key={item.id} + className={cn( + 'group relative rounded-[10px] bg-components-panel-bg', + `-ml-${handleSideWidth} min-h-[40px] px-0 py-0`, + )}> + <div > + <Item + className={cn(canDrag && 'handle')} + headerClassName={cn(canDrag && 'cursor-grab')} + nodeId={nodeId} + key={list[index].id} + payload={item} + onChange={handleClassChange(index)} + onRemove={handleRemoveClass(index)} + index={index + 1} + readonly={readonly} + filterVar={filterVar} + /> + </div> + </div> ) }) } @@ -81,7 +112,7 @@ const ClassList: FC<Props> = ({ /> )} - </div> + </ReactSortable> ) } export default React.memo(ClassList) diff --git a/web/app/components/workflow/nodes/question-classifier/panel.tsx b/web/app/components/workflow/nodes/question-classifier/panel.tsx index d2e0fb060a..8e27f5dceb 100644 --- a/web/app/components/workflow/nodes/question-classifier/panel.tsx +++ b/web/app/components/workflow/nodes/question-classifier/panel.tsx @@ -3,20 +3,16 @@ import React from 'react' import { useTranslation } from 'react-i18next' import VarReferencePicker from '../_base/components/variable/var-reference-picker' import ConfigVision from '../_base/components/config-vision' -import { findVariableWhenOnLLMVision } from '../utils' import useConfig from './use-config' import ClassList from './components/class-list' import AdvancedSetting from './components/advanced-setting' import type { QuestionClassifierNodeType } from './types' import Field from '@/app/components/workflow/nodes/_base/components/field' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' -import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' +import type { NodePanelProps } from '@/app/components/workflow/types' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' const i18nPrefix = 'workflow.nodes.questionClassifiers' @@ -38,66 +34,17 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({ hasSetBlockStatus, availableVars, availableNodesWithParent, - availableVisionVars, handleInstructionChange, - inputVarValues, - varInputs, - setInputVarValues, handleMemoryChange, isVisionModel, handleVisionResolutionChange, handleVisionResolutionEnabledChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - runResult, filterVar, - visionFiles, - setVisionFiles, + handleSortTopic, } = useConfig(id, data) const model = inputs.model - const singleRunForms = (() => { - const forms: FormProps[] = [] - - forms.push( - { - label: t('workflow.nodes.llm.singleRun.variable')!, - inputs: [{ - label: t(`${i18nPrefix}.inputVars`)!, - variable: 'query', - type: InputVarType.paragraph, - required: true, - }, ...varInputs], - values: inputVarValues, - onChange: setInputVarValues, - }, - ) - - if (isVisionModel && data.vision?.enabled && data.vision?.configs?.variable_selector) { - const currentVariable = findVariableWhenOnLLMVision(data.vision.configs.variable_selector, availableVisionVars) - - forms.push( - { - label: t('workflow.nodes.llm.vision')!, - inputs: [{ - label: currentVariable?.variable as any, - variable: '#files#', - type: currentVariable?.formType as any, - required: false, - }], - values: { '#files#': visionFiles }, - onChange: keyValue => setVisionFiles(keyValue['#files#']), - }, - ) - } - - return forms - })() - return ( <div className='pt-2'> <div className='space-y-4 px-4'> @@ -153,6 +100,7 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({ onChange={handleTopicsChange} readonly={readOnly} filterVar={filterVar} + handleSortTopic={handleSortTopic} /> </Field> <Split /> @@ -183,20 +131,14 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({ type='string' description={t(`${i18nPrefix}.outputVars.className`)} /> + <VarItem + name='usage' + type='object' + description={t(`${i18nPrefix}.outputVars.usage`)} + /> </> </OutputVars> </div> - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={singleRunForms} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/question-classifier/use-config.ts b/web/app/components/workflow/nodes/question-classifier/use-config.ts index 7df8293b40..a4acf5b7f6 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-config.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-config.ts @@ -9,14 +9,15 @@ import { import { useStore } from '../../store' import useAvailableVarList from '../_base/hooks/use-available-var-list' import useConfigVision from '../../hooks/use-config-vision' -import type { QuestionClassifierNodeType } from './types' +import type { QuestionClassifierNodeType, Topic } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' +import { useUpdateNodeInternals } from 'reactflow' const useConfig = (id: string, payload: QuestionClassifierNodeType) => { + const updateNodeInternals = useUpdateNodeInternals() const { nodesReadOnly: readOnly } = useNodesReadOnly() const isChatMode = useIsChatMode() const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type] @@ -87,7 +88,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { return setModelChanged(false) handleVisionConfigAfterModelChanged() - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isVisionModel, modelChanged]) const handleQueryVarChange = useCallback((newVar: ValueSelector | string) => { @@ -109,7 +110,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { query_variable_selector: inputs.query_variable_selector.length > 0 ? inputs.query_variable_selector : query_variable_selector, }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleClassesChange = useCallback((newClasses: any) => { @@ -163,63 +164,21 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { setInputs(newInputs) }, [inputs, setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - handleRun, - handleStop, - runInputData, - runInputDataRef, - setRunInputData, - runResult, - } = useOneStepRun<QuestionClassifierNodeType>({ - id, - data: inputs, - defaultRunInputData: { - 'query': '', - '#files#': [], - }, - }) - - const query = runInputData.query - const setQuery = useCallback((newQuery: string) => { - setRunInputData({ - ...runInputData, - query: newQuery, - }) - }, [runInputData, setRunInputData]) - - const varInputs = getInputVars([inputs.instruction]) - const inputVarValues = (() => { - const vars: Record<string, any> = { - query, - } - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - setRunInputData(newPayload) - }, [setRunInputData]) - - const visionFiles = runInputData['#files#'] - const setVisionFiles = useCallback((newFiles: any[]) => { - setRunInputData({ - ...runInputDataRef.current, - '#files#': newFiles, - }) - }, [runInputDataRef, setRunInputData]) - const filterVar = useCallback((varPayload: Var) => { return varPayload.type === VarType.string }, []) + const handleSortTopic = useCallback((newTopics: (Topic & { id: string })[]) => { + const newInputs = produce(inputs, (draft) => { + draft.classes = newTopics.filter(Boolean).map(item => ({ + id: item.id, + name: item.name, + })) + }) + setInputs(newInputs) + updateNodeInternals(id) + }, [id, inputs, setInputs, updateNodeInternals]) + return { readOnly, inputs, @@ -235,23 +194,11 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { availableNodesWithParent, availableVisionVars, handleInstructionChange, - varInputs, - inputVarValues, - setInputVarValues, handleMemoryChange, isVisionModel, handleVisionResolutionEnabledChange, handleVisionResolutionChange, - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - query, - setQuery, - runResult, - visionFiles, - setVisionFiles, + handleSortTopic, } } diff --git a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts new file mode 100644 index 0000000000..66755abb6e --- /dev/null +++ b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts @@ -0,0 +1,146 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { InputVar, Var, Variable } from '@/app/components/workflow/types' +import { InputVarType, VarType } from '@/app/components/workflow/types' +import type { QuestionClassifierNodeType } from './types' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { useCallback } from 'react' +import useConfigVision from '../../hooks/use-config-vision' +import { noop } from 'lodash-es' +import { findVariableWhenOnLLMVision } from '../utils' +import useAvailableVarList from '../_base/hooks/use-available-var-list' + +const i18nPrefix = 'workflow.nodes.questionClassifiers' + +type Params = { + id: string, + payload: QuestionClassifierNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + runInputDataRef, + getInputVars, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud<QuestionClassifierNodeType>(id, payload) + + const model = inputs.model + + const { + isVisionModel, + } = useConfigVision(model, { + payload: inputs.vision, + onChange: noop, + }) + + const visionFiles = runInputData['#files#'] + const setVisionFiles = useCallback((newFiles: any[]) => { + setRunInputData?.({ + ...runInputDataRef.current, + '#files#': newFiles, + }) + }, [runInputDataRef, setRunInputData]) + + const varInputs = getInputVars([inputs.instruction]) + + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .filter(key => !['#files#'].includes(key)) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + const newVars = { + ...newPayload, + '#files#': runInputDataRef.current['#files#'], + } + setRunInputData?.(newVars) + }, [runInputDataRef, setRunInputData]) + + const filterVisionInputVar = useCallback((varPayload: Var) => { + return [VarType.file, VarType.arrayFile].includes(varPayload.type) + }, []) + const { + availableVars: availableVisionVars, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterVisionInputVar, + }) + + const forms = (() => { + const forms: FormProps[] = [] + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs: [{ + label: t(`${i18nPrefix}.inputVars`)!, + variable: 'query', + type: InputVarType.paragraph, + required: true, + }, ...varInputs], + values: inputVarValues, + onChange: setInputVarValues, + }, + ) + + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const currentVariable = findVariableWhenOnLLMVision(payload.vision.configs.variable_selector, availableVisionVars) + + forms.push( + { + label: t('workflow.nodes.llm.vision')!, + inputs: [{ + label: currentVariable?.variable as any, + variable: '#files#', + type: currentVariable?.formType as any, + required: false, + }], + values: { '#files#': visionFiles }, + onChange: keyValue => setVisionFiles(keyValue['#files#']), + }, + ) + } + return forms + })() + + const getDependentVars = () => { + const promptVars = varInputs.map(item => item.variable.slice(1, -1).split('.')) + const vars = [payload.query_variable_selector, ...promptVars] + if (isVisionModel && payload.vision?.enabled && payload.vision?.configs?.variable_selector) { + const visionVar = payload.vision.configs.variable_selector + vars.push(visionVar) + } + return vars + } + + const getDependentVar = (variable: string) => { + if(variable === 'query') + return payload.query_variable_selector + if(variable === '#files#') + return payload.vision.configs?.variable_selector + + return false + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/start/components/var-item.tsx b/web/app/components/workflow/nodes/start/components/var-item.tsx index 68dc141d75..029547542e 100644 --- a/web/app/components/workflow/nodes/start/components/var-item.tsx +++ b/web/app/components/workflow/nodes/start/components/var-item.tsx @@ -13,18 +13,22 @@ import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general' import Badge from '@/app/components/base/badge' import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal' import { noop } from 'lodash-es' +import cn from '@/utils/classnames' type Props = { + className?: string readonly: boolean payload: InputVar onChange?: (item: InputVar, moreInfo?: MoreInfo) => void onRemove?: () => void rightContent?: React.JSX.Element varKeys?: string[] - showLegacyBadge?: boolean + showLegacyBadge?: boolean, + canDrag?: boolean, } const VarItem: FC<Props> = ({ + className, readonly, payload, onChange = noop, @@ -32,6 +36,7 @@ const VarItem: FC<Props> = ({ rightContent, varKeys = [], showLegacyBadge = false, + canDrag, }) => { const { t } = useTranslation() @@ -47,9 +52,9 @@ const VarItem: FC<Props> = ({ hideEditVarModal() }, [onChange, hideEditVarModal]) return ( - <div ref={ref} className='flex h-8 cursor-pointer items-center justify-between rounded-lg border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 shadow-xs hover:shadow-md'> + <div ref={ref} className={cn('flex h-8 cursor-pointer items-center justify-between rounded-lg border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 shadow-xs hover:shadow-md', className)}> <div className='flex w-0 grow items-center space-x-1'> - <Variable02 className='h-3.5 w-3.5 text-text-accent' /> + <Variable02 className={cn('h-3.5 w-3.5 text-text-accent', canDrag && 'group-hover:opacity-0')} /> <div title={payload.variable} className='max-w-[130px] shrink-0 truncate text-[13px] font-medium text-text-secondary'>{payload.variable}</div> {payload.label && (<><div className='shrink-0 text-xs font-medium text-text-quaternary'>·</div> <div title={payload.label as string} className='max-w-[130px] truncate text-[13px] font-medium text-text-tertiary'>{payload.label as string}</div> diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index 7eccbec618..024b50a759 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -1,10 +1,15 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useCallback, useMemo } from 'react' import produce from 'immer' import { useTranslation } from 'react-i18next' import VarItem from './var-item' import { ChangeType, type InputVar, type MoreInfo } from '@/app/components/workflow/types' +import { v4 as uuid4 } from 'uuid' +import { ReactSortable } from 'react-sortablejs' +import { RiDraggable } from '@remixicon/react' +import cn from '@/utils/classnames' + type Props = { readonly: boolean list: InputVar[] @@ -44,6 +49,16 @@ const VarList: FC<Props> = ({ } }, [list, onChange]) + const listWithIds = useMemo(() => list.map((item) => { + const id = uuid4() + return { + id, + variable: { ...item }, + } + }), [list]) + + const varCount = list.length + if (list.length === 0) { return ( <div className='flex h-[42px] items-center justify-center rounded-md bg-components-panel-bg text-xs font-normal leading-[18px] text-text-tertiary'> @@ -53,18 +68,39 @@ const VarList: FC<Props> = ({ } return ( - <div className='space-y-1'> - {list.map((item, index) => ( - <VarItem - key={index} - readonly={readonly} - payload={item} - onChange={handleVarChange(index)} - onRemove={handleVarRemove(index)} - varKeys={list.map(item => item.variable)} - /> - ))} - </div> + <ReactSortable + className='space-y-1' + list={listWithIds} + setList={(list) => { onChange(list.map(item => item.variable)) }} + handle='.handle' + ghostClass='opacity-50' + animation={150} + > + {list.map((item, index) => { + const canDrag = (() => { + if (readonly) + return false + return varCount > 1 + })() + return ( + <div key={index} className='group relative'> + <VarItem + className={cn(canDrag && 'handle')} + readonly={readonly} + payload={item} + onChange={handleVarChange(index)} + onRemove={handleVarRemove(index)} + varKeys={list.map(item => item.variable)} + canDrag={canDrag} + /> + {canDrag && <RiDraggable className={cn( + 'handle absolute left-3 top-2.5 hidden h-3 w-3 cursor-pointer text-text-tertiary', + 'group-hover:block', + )} />} + </div> + ) + })} + </ReactSortable> ) } export default React.memo(VarList) diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index e30e8c2838..c0ade614e0 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -10,6 +10,7 @@ import { useNodesReadOnly, useWorkflow, } from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: StartNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -18,6 +19,13 @@ const useConfig = (id: string, payload: StartNodeType) => { const { inputs, setInputs } = useNodeCrud<StartNodeType>(id, payload) + const { + deleteNodeInspectorVars, + renameInspectVarName, + nodesWithInspectVars, + deleteInspectVar, + } = useInspectVarsCrud() + const [isShowAddVarModal, { setTrue: showAddVarModal, setFalse: hideAddVarModal, @@ -31,6 +39,12 @@ const useConfig = (id: string, payload: StartNodeType) => { const [removedIndex, setRemoveIndex] = useState(0) const handleVarListChange = useCallback((newList: InputVar[], moreInfo?: { index: number; payload: MoreInfo }) => { if (moreInfo?.payload?.type === ChangeType.remove) { + const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => { + return varItem.name === moreInfo?.payload?.payload?.beforeKey + })?.id + if(varId) + deleteInspectVar(id, varId) + if (isVarUsedInNodes([id, moreInfo?.payload?.payload?.beforeKey || ''])) { showRemoveVarConfirm() setRemovedVar([id, moreInfo?.payload?.payload?.beforeKey || '']) @@ -46,8 +60,12 @@ const useConfig = (id: string, payload: StartNodeType) => { if (moreInfo?.payload?.type === ChangeType.changeVarName) { const changedVar = newList[moreInfo.index] handleOutVarRenameChange(id, [id, inputs.variables[moreInfo.index].variable], [id, changedVar.variable]) + renameInspectVarName(id, inputs.variables[moreInfo.index].variable, changedVar.variable) + } + else if(moreInfo?.payload?.type !== ChangeType.remove) { // edit var type + deleteNodeInspectorVars(id) } - }, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) + }, [deleteInspectVar, deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, nodesWithInspectVars, renameInspectVarName, setInputs, showRemoveVarConfirm]) const removeVarInNode = useCallback(() => { const newInputs = produce(inputs, (draft) => { diff --git a/web/app/components/workflow/nodes/start/use-single-run-form-params.ts b/web/app/components/workflow/nodes/start/use-single-run-form-params.ts new file mode 100644 index 0000000000..38abbf2a63 --- /dev/null +++ b/web/app/components/workflow/nodes/start/use-single-run-form-params.ts @@ -0,0 +1,87 @@ +import type { MutableRefObject } from 'react' +import { useTranslation } from 'react-i18next' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import type { ValueSelector } from '@/app/components/workflow/types' +import { type InputVar, InputVarType, type Variable } from '@/app/components/workflow/types' +import type { StartNodeType } from './types' +import { useIsChatMode } from '../../hooks' + +type Params = { + id: string, + payload: StartNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + setRunInputData, +}: Params) => { + const { t } = useTranslation() + const isChatMode = useIsChatMode() + + const forms = (() => { + const forms: FormProps[] = [] + const inputs: InputVar[] = payload.variables.map((item) => { + return { + ...item, + getVarValueFromDependent: true, + } + }) + + if (isChatMode) { + inputs.push({ + label: 'sys.query', + variable: '#sys.query#', + type: InputVarType.textInput, + required: true, + }) + } + + inputs.push({ + label: 'sys.files', + variable: '#sys.files#', + type: InputVarType.multiFiles, + required: false, + }) + + forms.push( + { + label: t('workflow.nodes.llm.singleRun.variable')!, + inputs, + values: runInputData, + onChange: setRunInputData, + }, + ) + + return forms + })() + + const getDependentVars = () => { + const inputVars = payload.variables.map((item) => { + return [id, item.variable] + }) + const vars: ValueSelector[] = [...inputVars, ['sys', 'files']] + + if (isChatMode) + vars.push(['sys', 'query']) + + return vars + } + + const getDependentVar = (variable: string) => { + return [id, variable] + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/template-transform/panel.tsx b/web/app/components/workflow/nodes/template-transform/panel.tsx index e120482925..29c34ee663 100644 --- a/web/app/components/workflow/nodes/template-transform/panel.tsx +++ b/web/app/components/workflow/nodes/template-transform/panel.tsx @@ -14,8 +14,6 @@ import Split from '@/app/components/workflow/nodes/_base/components/split' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import type { NodePanelProps } from '@/app/components/workflow/types' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' -import ResultPanel from '@/app/components/workflow/run/result-panel' const i18nPrefix = 'workflow.nodes.templateTransform' @@ -35,16 +33,6 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({ handleAddEmptyVariable, handleCodeChange, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, } = useConfig(id, data) return ( @@ -106,23 +94,6 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({ </> </OutputVars> </div> - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - onHide={hideSingleRun} - forms={[ - { - inputs: varInputs, - values: inputVarValues, - onChange: setInputVarValues, - }, - ]} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - result={<ResultPanel {...runResult} showSteps={false} />} - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/template-transform/use-config.ts b/web/app/components/workflow/nodes/template-transform/use-config.ts index e0c41ac2d6..8be93abdf8 100644 --- a/web/app/components/workflow/nodes/template-transform/use-config.ts +++ b/web/app/components/workflow/nodes/template-transform/use-config.ts @@ -6,7 +6,6 @@ import { VarType } from '../../types' import { useStore } from '../../store' import type { TemplateTransformNodeType } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' import { useNodesReadOnly, } from '@/app/components/workflow/hooks' @@ -66,7 +65,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { ...defaultConfig, }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultConfig]) const handleCodeChange = useCallback((template: string) => { @@ -76,37 +75,6 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { setInputs(newInputs) }, [setInputs]) - // single run - const { - isShowSingleRun, - hideSingleRun, - toVarInputs, - runningStatus, - handleRun, - handleStop, - runInputData, - setRunInputData, - runResult, - } = useOneStepRun<TemplateTransformNodeType>({ - id, - data: inputs, - defaultRunInputData: {}, - }) - const varInputs = toVarInputs(inputs.variables) - - const inputVarValues = (() => { - const vars: Record<string, any> = {} - Object.keys(runInputData) - .forEach((key) => { - vars[key] = runInputData[key] - }) - return vars - })() - - const setInputVarValues = useCallback((newPayload: Record<string, any>) => { - setRunInputData(newPayload) - }, [setRunInputData]) - const filterVar = useCallback((varPayload: Var) => { return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type) }, []) @@ -121,16 +89,6 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => { handleAddEmptyVariable, handleCodeChange, filterVar, - // single run - isShowSingleRun, - hideSingleRun, - runningStatus, - handleRun, - handleStop, - varInputs, - inputVarValues, - setInputVarValues, - runResult, } } diff --git a/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts b/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts new file mode 100644 index 0000000000..ab1cfe731d --- /dev/null +++ b/web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts @@ -0,0 +1,65 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import type { TemplateTransformNodeType } from './types' + +type Params = { + id: string, + payload: TemplateTransformNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] +} +const useSingleRunFormParams = ({ + id, + payload, + runInputData, + toVarInputs, + setRunInputData, +}: Params) => { + const { inputs } = useNodeCrud<TemplateTransformNodeType>(id, payload) + + const varInputs = toVarInputs(inputs.variables) + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = useMemo(() => { + return [ + { + inputs: varInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + }, [inputVarValues, setInputVarValues, varInputs]) + + const getDependentVars = () => { + return payload.variables.map(v => v.value_selector) + } + + const getDependentVar = (variable: string) => { + const varItem = payload.variables.find(v => v.variable === variable) + if (varItem) + return varItem.value_selector + } + + return { + forms, + getDependentVars, + getDependentVar, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/tool/components/copy-id.tsx b/web/app/components/workflow/nodes/tool/components/copy-id.tsx new file mode 100644 index 0000000000..3a633e1d2e --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/copy-id.tsx @@ -0,0 +1,51 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiFileCopyLine } from '@remixicon/react' +import copy from 'copy-to-clipboard' +import { debounce } from 'lodash-es' +import Tooltip from '@/app/components/base/tooltip' + +type Props = { + content: string +} + +const prefixEmbedded = 'appOverview.overview.appInfo.embedded' + +const CopyFeedbackNew = ({ content }: Props) => { + const { t } = useTranslation() + const [isCopied, setIsCopied] = useState<boolean>(false) + + const onClickCopy = debounce(() => { + copy(content) + setIsCopied(true) + }, 100) + + const onMouseLeave = debounce(() => { + setIsCopied(false) + }, 100) + + return ( + <div className='inline-flex pb-0.5' onClick={e => e.stopPropagation()} onMouseLeave={onMouseLeave}> + <Tooltip + popupContent={ + (isCopied + ? t(`${prefixEmbedded}.copied`) + : t(`${prefixEmbedded}.copy`)) || '' + } + > + <div + className='group/copy flex items-center gap-0.5 ' + onClick={onClickCopy} + > + <div + className='system-2xs-regular cursor-pointer text-text-quaternary group-hover:text-text-tertiary' + >{content}</div> + <RiFileCopyLine className='h-3 w-3 text-text-tertiary opacity-0 group-hover/copy:opacity-100' /> + </div> + </Tooltip> + </div> + ) +} + +export default CopyFeedbackNew diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 1a609c58f5..244e54a5f2 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import type { ToolVarInputs } from '../types' import { VarType as VarKindType } from '../types' import cn from '@/utils/classnames' -import type { ValueSelector, Var } from '@/app/components/workflow/types' +import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -17,6 +17,7 @@ import { VarType } from '@/app/components/workflow/types' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' import { noop } from 'lodash-es' +import type { Tool } from '@/app/components/tools/types' type Props = { readOnly: boolean @@ -27,6 +28,8 @@ type Props = { onOpen?: (index: number) => void isSupportConstantValue?: boolean filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean + currentTool?: Tool + currentProvider?: ToolWithProvider } const InputVarList: FC<Props> = ({ @@ -38,6 +41,8 @@ const InputVarList: FC<Props> = ({ onOpen = noop, isSupportConstantValue, filterVar, + currentTool, + currentProvider, }) => { const language = useLanguage() const { t } = useTranslation() @@ -58,6 +63,8 @@ const InputVarList: FC<Props> = ({ return 'ModelSelector' else if (type === FormTypeEnum.toolSelector) return 'ToolSelector' + else if (type === FormTypeEnum.dynamicSelect || type === FormTypeEnum.select) + return 'Select' else return 'String' } @@ -149,6 +156,7 @@ const InputVarList: FC<Props> = ({ const handleOpen = useCallback((index: number) => { return () => onOpen(index) }, [onOpen]) + return ( <div className='space-y-3'> { @@ -163,7 +171,8 @@ const InputVarList: FC<Props> = ({ } = schema const varInput = value[variable] const isNumber = type === FormTypeEnum.textNumber - const isSelect = type === FormTypeEnum.select + const isDynamicSelect = type === FormTypeEnum.dynamicSelect + const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files const isAppSelector = type === FormTypeEnum.appSelector const isModelSelector = type === FormTypeEnum.modelSelector @@ -198,11 +207,13 @@ const InputVarList: FC<Props> = ({ value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])} onChange={handleNotMixedTypeChange(variable)} onOpen={handleOpen(index)} - defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)} + defaultVarKindType={varInput?.type || ((isNumber || isDynamicSelect) ? VarKindType.constant : VarKindType.variable)} isSupportConstantValue={isSupportConstantValue} filterVar={isNumber ? filterVar : undefined} availableVars={isSelect ? availableVars : undefined} schema={schema} + currentTool={currentTool} + currentProvider={currentProvider} /> )} {isFile && ( diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx new file mode 100644 index 0000000000..6680c8ebb6 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -0,0 +1,62 @@ +import { + memo, +} from 'react' +import { useTranslation } from 'react-i18next' +import PromptEditor from '@/app/components/base/prompt-editor' +import Placeholder from './placeholder' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' +import { BlockEnum } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type MixedVariableTextInputProps = { + readOnly?: boolean + nodesOutputVars?: NodeOutPutVar[] + availableNodes?: Node[] + value?: string + onChange?: (text: string) => void +} +const MixedVariableTextInput = ({ + readOnly = false, + nodesOutputVars, + availableNodes = [], + value = '', + onChange, +}: MixedVariableTextInputProps) => { + const { t } = useTranslation() + return ( + <PromptEditor + wrapperClassName={cn( + 'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1', + 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover', + 'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs', + )} + className='caret:text-text-accent' + editable={!readOnly} + value={value} + workflowVariableBlock={{ + show: true, + variables: nodesOutputVars || [], + workflowNodesMap: availableNodes.reduce((acc, node) => { + acc[node.id] = { + title: node.data.title, + type: node.data.type, + } + if (node.data.type === BlockEnum.Start) { + acc.sys = { + title: t('workflow.blocks.start'), + type: BlockEnum.Start, + } + } + return acc + }, {} as any), + }} + placeholder={<Placeholder />} + onChange={onChange} + /> + ) +} + +export default memo(MixedVariableTextInput) diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx new file mode 100644 index 0000000000..3337d6ae66 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { FOCUS_COMMAND } from 'lexical' +import { $insertNodes } from 'lexical' +import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node' +import Badge from '@/app/components/base/badge' + +const Placeholder = () => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const handleInsert = useCallback((text: string) => { + editor.update(() => { + const textNode = new CustomTextNode(text) + $insertNodes([textNode]) + }) + editor.dispatchCommand(FOCUS_COMMAND, undefined as any) + }, [editor]) + + return ( + <div + className='pointer-events-auto flex h-full w-full cursor-text items-center px-2' + onClick={(e) => { + e.stopPropagation() + handleInsert('') + }} + > + <div className='flex grow items-center'> + {t('workflow.nodes.tool.insertPlaceholder1')} + <div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div> + <div + className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary' + onClick={((e) => { + e.stopPropagation() + handleInsert('/') + })} + > + {t('workflow.nodes.tool.insertPlaceholder2')} + </div> + </div> + <Badge + className='shrink-0' + text='String' + uppercase={false} + /> + </div> + ) +} + +export default Placeholder diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx new file mode 100644 index 0000000000..a867797473 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx @@ -0,0 +1,51 @@ +'use client' +import type { FC } from 'react' +import type { ToolVarInputs } from '../../types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import ToolFormItem from './item' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema[] + value: ToolVarInputs + onChange: (value: ToolVarInputs) => void + onOpen?: (index: number) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const ToolForm: FC<Props> = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + return ( + <div className='space-y-1'> + { + schema.map((schema, index) => ( + <ToolFormItem + key={index} + readOnly={readOnly} + nodeId={nodeId} + schema={schema} + value={value} + onChange={onChange} + inPanel={inPanel} + currentTool={currentTool} + currentProvider={currentProvider} + /> + )) + } + </div> + ) +} +export default ToolForm diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx new file mode 100644 index 0000000000..11de42fe56 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx @@ -0,0 +1,105 @@ +'use client' +import type { FC } from 'react' +import { + RiBracesLine, +} from '@remixicon/react' +import type { ToolVarInputs } from '../../types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' +import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item' +import { useBoolean } from 'ahooks' +import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema + value: ToolVarInputs + onChange: (value: ToolVarInputs) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const ToolFormItem: FC<Props> = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + const language = useLanguage() + const { name, label, type, required, tooltip, input_schema } = schema + const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array + const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput + const [isShowSchema, { + setTrue: showSchema, + setFalse: hideSchema, + }] = useBoolean(false) + return ( + <div className='space-y-0.5 py-1'> + <div> + <div className='flex h-6 items-center'> + <div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div> + {required && ( + <div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div> + )} + {!showDescription && tooltip && ( + <Tooltip + popupContent={<div className='w-[200px]'> + {tooltip[language] || tooltip.en_US} + </div>} + triggerClassName='ml-1 w-4 h-4' + asChild={false} + /> + )} + {showSchemaButton && ( + <> + <div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div> + <Button + variant='ghost' + size='small' + onClick={showSchema} + className='system-xs-regular px-1 text-text-tertiary' + > + <RiBracesLine className='mr-1 size-3.5' /> + <span>JSON Schema</span> + </Button> + </> + )} + </div> + {showDescription && tooltip && ( + <div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div> + )} + </div> + <FormInputItem + readOnly={readOnly} + nodeId={nodeId} + schema={schema} + value={value} + onChange={onChange} + inPanel={inPanel} + currentTool={currentTool} + currentProvider={currentProvider} + /> + + {isShowSchema && ( + <SchemaModal + isShow + onClose={hideSchema} + rootName={name} + schema={input_schema!} + /> + )} + </div> + ) +} +export default ToolFormItem diff --git a/web/app/components/workflow/nodes/tool/default.ts b/web/app/components/workflow/nodes/tool/default.ts index f245929684..1fdb9eed2d 100644 --- a/web/app/components/workflow/nodes/tool/default.ts +++ b/web/app/components/workflow/nodes/tool/default.ts @@ -10,6 +10,7 @@ const nodeDefault: NodeDefault<ToolNodeType> = { defaultValue: { tool_parameters: {}, tool_configurations: {}, + version: '2', }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode @@ -55,6 +56,8 @@ const nodeDefault: NodeDefault<ToolNodeType> = { const value = payload.tool_configurations[field.variable] if (!errorMessages && (value === undefined || value === null || value === '')) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] }) + if (!errorMessages && typeof value === 'object' && !!value.type && (value.value === undefined || value.value === null || value.value === '' || (Array.isArray(value.value) && value.value.length === 0))) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] }) }) } diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index f3cb4d9fae..e15ddcaaaa 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -21,14 +21,14 @@ const Node: FC<NodeProps<ToolNodeType>> = ({ <div title={key} className='max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary'> {key} </div> - {typeof tool_configurations[key] === 'string' && ( + {typeof tool_configurations[key].value === 'string' && ( <div title={tool_configurations[key]} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'> - {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key]} + {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value} </div> )} - {typeof tool_configurations[key] === 'number' && ( + {typeof tool_configurations[key].value === 'number' && ( <div title={tool_configurations[key].toString()} className='w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary'> - {tool_configurations[key]} + {tool_configurations[key].value} </div> )} {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( @@ -36,11 +36,6 @@ const Node: FC<NodeProps<ToolNodeType>> = ({ {tool_configurations[key].model} </div> )} - {/* {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.appSelector && ( - <div title={tool_configurations[key].app_id} className='grow w-0 shrink-0 truncate text-right text-xs font-normal text-gray-700'> - {tool_configurations[key].app_id} - </div> - )} */} </div> ))} diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 393a11c1e8..936f730a46 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -1,22 +1,16 @@ import type { FC } from 'react' -import React, { useMemo } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' import Split from '../_base/components/split' import type { ToolNodeType } from './types' import useConfig from './use-config' -import InputVarList from './components/input-var-list' +import ToolForm from './components/tool-form' import Button from '@/app/components/base/button' import Field from '@/app/components/workflow/nodes/_base/components/field' import type { NodePanelProps } from '@/app/components/workflow/types' -import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import Loading from '@/app/components/base/loading' -import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import ResultPanel from '@/app/components/workflow/run/result-panel' -import { useToolIcon } from '@/app/components/workflow/hooks' -import { useLogs } from '@/app/components/workflow/run/hooks' -import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log' import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' import { Type } from '../llm/types' @@ -33,8 +27,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ inputs, toolInputVarSchema, setInputVar, - handleOnVarOpen, - filterVar, toolSettingSchema, toolSettingValue, setToolSettingValue, @@ -45,23 +37,12 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ hideSetAuthModal, handleSaveAuth, isLoading, - isShowSingleRun, - hideSingleRun, - singleRunForms, - runningStatus, - handleRun, - handleStop, - runResult, outputSchema, hasObjectOutput, + currTool, } = useConfig(id, data) - const toolIcon = useToolIcon(data) - const logsParams = useLogs() - const nodeInfo = useMemo(() => { - if (!runResult) - return null - return formatToTracingNodeList([runResult], t)[0] - }, [runResult, t]) + + const [collapsed, setCollapsed] = React.useState(false) if (isLoading) { return <div className='flex h-[200px] items-center justify-center'> @@ -84,44 +65,49 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ </div> </> )} - {!isShowAuthBtn && <> - <div className='space-y-4 px-4'> + {!isShowAuthBtn && ( + <div className='relative'> {toolInputVarSchema.length > 0 && ( <Field + className='px-4' title={t(`${i18nPrefix}.inputVars`)} > - <InputVarList + <ToolForm readOnly={readOnly} nodeId={id} schema={toolInputVarSchema as any} value={inputs.tool_parameters} onChange={setInputVar} - filterVar={filterVar} - isSupportConstantValue - onOpen={handleOnVarOpen} + currentProvider={currCollection} + currentTool={currTool} /> </Field> )} {toolInputVarSchema.length > 0 && toolSettingSchema.length > 0 && ( - <Split /> + <Split className='mt-1' /> )} - <Form - className='space-y-4' - itemClassName='!py-0' - fieldLabelClassName='!text-[13px] !font-semibold !text-text-secondary uppercase' - value={toolSettingValue} - onChange={setToolSettingValue} - formSchemas={toolSettingSchema as any} - isEditMode={false} - showOnVariableMap={{}} - validating={false} - // inputClassName='!bg-gray-50' - readonly={readOnly} - /> + {toolSettingSchema.length > 0 && ( + <> + <OutputVars + title={t(`${i18nPrefix}.settings`)} + collapsed={collapsed} + onCollapse={setCollapsed} + > + <ToolForm + readOnly={readOnly} + nodeId={id} + schema={toolSettingSchema as any} + value={toolSettingValue} + onChange={setToolSettingValue} + /> + </OutputVars> + <Split /> + </> + )} </div> - </>} + )} {showSetAuth && ( <ConfigCredential @@ -180,21 +166,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ </> </OutputVars> </div> - - {isShowSingleRun && ( - <BeforeRunForm - nodeName={inputs.title} - nodeType={inputs.type} - toolIcon={toolIcon} - onHide={hideSingleRun} - forms={singleRunForms} - runningStatus={runningStatus} - onRun={handleRun} - onStop={handleStop} - {...logsParams} - result={<ResultPanel {...runResult} showSteps={false} {...logsParams} nodeInfo={nodeInfo} />} - /> - )} </div> ) } diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 4b78c53ab2..4584645a1e 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -22,4 +22,5 @@ export type ToolNodeType = CommonNodeType & { tool_configurations: Record<string, any> output_schema: Record<string, any> paramSchemas?: Record<string, any>[] + version?: string } diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts index 38ca5b5195..ea8d0e21ca 100644 --- a/web/app/components/workflow/nodes/tool/use-config.ts +++ b/web/app/components/workflow/nodes/tool/use-config.ts @@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next' import produce from 'immer' import { useBoolean } from 'ahooks' import { useStore } from '../../store' -import { type ToolNodeType, type ToolVarInputs, VarType } from './types' +import type { ToolNodeType, ToolVarInputs } from './types' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { CollectionType } from '@/app/components/tools/types' import { updateBuiltInToolCredential } from '@/service/tools' -import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import { + getConfiguredValue, + toolParametersToFormSchemas, +} from '@/app/components/tools/utils/to-form-schema' import Toast from '@/app/components/base/toast' -import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' -import { VarType as VarVarType } from '@/app/components/workflow/types' -import type { InputVar, ValueSelector, Var } from '@/app/components/workflow/types' -import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run' +import type { InputVar } from '@/app/components/workflow/types' import { useFetchToolsData, useNodesReadOnly, @@ -28,17 +28,18 @@ const useConfig = (id: string, payload: ToolNodeType) => { const language = useLanguage() const { inputs, setInputs: doSetInputs } = useNodeCrud<ToolNodeType>(id, payload) /* - * tool_configurations: tool setting, not dynamic setting - * tool_parameters: tool dynamic setting(by user) + * tool_configurations: tool setting, not dynamic setting (form type = form) + * tool_parameters: tool dynamic setting(form type = llm) * output_schema: tool dynamic output */ - const { provider_id, provider_type, tool_name, tool_configurations, output_schema } = inputs + const { provider_id, provider_type, tool_name, tool_configurations, output_schema, tool_parameters } = inputs const isBuiltIn = provider_type === CollectionType.builtIn const buildInTools = useStore(s => s.buildInTools) const customTools = useStore(s => s.customTools) const workflowTools = useStore(s => s.workflowTools) + const mcpTools = useStore(s => s.mcpTools) - const currentTools = (() => { + const currentTools = useMemo(() => { switch (provider_type) { case CollectionType.builtIn: return buildInTools @@ -46,10 +47,12 @@ const useConfig = (id: string, payload: ToolNodeType) => { return customTools case CollectionType.workflow: return workflowTools + case CollectionType.mcp: + return mcpTools default: return [] } - })() + }, [buildInTools, customTools, mcpTools, provider_type, workflowTools]) const currCollection = currentTools.find(item => canFindTool(item.id, provider_id)) // Auth @@ -93,10 +96,10 @@ const useConfig = (id: string, payload: ToolNodeType) => { const value = newConfig[key] if (schema?.type === 'boolean') { if (typeof value === 'string') - newConfig[key] = Number.parseInt(value, 10) + newConfig[key] = value === 'true' || value === '1' - if (typeof value === 'boolean') - newConfig[key] = value ? 1 : 0 + if (typeof value === 'number') + newConfig[key] = value === 1 } if (schema?.type === 'number-input') { @@ -109,12 +112,11 @@ const useConfig = (id: string, payload: ToolNodeType) => { doSetInputs(newInputs) }, [doSetInputs, formSchemas, hasShouldTransferTypeSettingInput]) const [notSetDefaultValue, setNotSetDefaultValue] = useState(false) - const toolSettingValue = (() => { + const toolSettingValue = useMemo(() => { if (notSetDefaultValue) return tool_configurations - - return addDefaultValue(tool_configurations, toolSettingSchema) - })() + return getConfiguredValue(tool_configurations, toolSettingSchema) + }, [notSetDefaultValue, toolSettingSchema, tool_configurations]) const setToolSettingValue = useCallback((value: Record<string, any>) => { setNotSetDefaultValue(true) setInputs({ @@ -123,16 +125,20 @@ const useConfig = (id: string, payload: ToolNodeType) => { }) }, [inputs, setInputs]) - useEffect(() => { - if (!currTool) - return + const formattingParameters = () => { const inputsWithDefaultValue = produce(inputs, (draft) => { if (!draft.tool_configurations || Object.keys(draft.tool_configurations).length === 0) - draft.tool_configurations = addDefaultValue(tool_configurations, toolSettingSchema) - - if (!draft.tool_parameters) - draft.tool_parameters = {} + draft.tool_configurations = getConfiguredValue(tool_configurations, toolSettingSchema) + if (!draft.tool_parameters || Object.keys(draft.tool_parameters).length === 0) + draft.tool_parameters = getConfiguredValue(tool_parameters, toolInputVarSchema) }) + return inputsWithDefaultValue + } + + useEffect(() => { + if (!currTool) + return + const inputsWithDefaultValue = formattingParameters() setInputs(inputsWithDefaultValue) // eslint-disable-next-line react-hooks/exhaustive-deps }, [currTool]) @@ -145,54 +151,10 @@ const useConfig = (id: string, payload: ToolNodeType) => { }) }, [inputs, setInputs]) - const [currVarIndex, setCurrVarIndex] = useState(-1) - const currVarType = toolInputVarSchema[currVarIndex]?._type - const handleOnVarOpen = useCallback((index: number) => { - setCurrVarIndex(index) - }, []) - - const filterVar = useCallback((varPayload: Var) => { - if (currVarType) - return varPayload.type === currVarType - - return varPayload.type !== VarVarType.arrayFile - }, [currVarType]) - const isLoading = currTool && (isBuiltIn ? !currCollection : false) - // single run - const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({}) - const setInputVarValues = (value: Record<string, any>) => { - doSetInputVarValues(value) - // eslint-disable-next-line ts/no-use-before-define - setRunInputData(value) - } - // fill single run form variable with constant value first time - const inputVarValuesWithConstantValue = () => { - const res = produce(inputVarValues, (draft) => { - Object.keys(inputs.tool_parameters).forEach((key: string) => { - const { type, value } = inputs.tool_parameters[key] - if (type === VarType.constant && (value === undefined || value === null)) - draft.tool_parameters[key].value = value - }) - }) - return res - } - - const { - isShowSingleRun, - hideSingleRun, - getInputVars, - runningStatus, - setRunInputData, - handleRun: doHandleRun, - handleStop, - runResult, - } = useOneStepRun<ToolNodeType>({ - id, - data: inputs, - defaultRunInputData: {}, - moreDataForCheckValid: { + const getMoreDataForCheckValid = () => { + return { toolInputsSchema: (() => { const formInputs: InputVar[] = [] toolInputVarSchema.forEach((item: any) => { @@ -208,52 +170,7 @@ const useConfig = (id: string, payload: ToolNodeType) => { notAuthed: isShowAuthBtn, toolSettingSchema, language, - }, - }) - - const hadVarParams = Object.keys(inputs.tool_parameters) - .filter(key => inputs.tool_parameters[key].type !== VarType.constant) - .map(k => inputs.tool_parameters[k]) - - const varInputs = getInputVars(hadVarParams.map((p) => { - if (p.type === VarType.variable) { - // handle the old wrong value not crash the page - if (!(p.value as any).join) - return `{{#${p.value}#}}` - - return `{{#${(p.value as ValueSelector).join('.')}#}}` } - - return p.value as string - })) - - const singleRunForms = (() => { - const forms: FormProps[] = [{ - inputs: varInputs, - values: inputVarValuesWithConstantValue(), - onChange: setInputVarValues, - }] - return forms - })() - - const handleRun = (submitData: Record<string, any>) => { - const varTypeInputKeys = Object.keys(inputs.tool_parameters) - .filter(key => inputs.tool_parameters[key].type === VarType.variable) - const shouldAdd = varTypeInputKeys.length > 0 - if (!shouldAdd) { - doHandleRun(submitData) - return - } - const addMissedVarData = { ...submitData } - Object.keys(submitData).forEach((key) => { - const value = submitData[key] - varTypeInputKeys.forEach((inputKey) => { - const inputValue = inputs.tool_parameters[inputKey].value as ValueSelector - if (`#${inputValue.join('.')}#` === key) - addMissedVarData[inputKey] = value - }) - }) - doHandleRun(addMissedVarData) } const outputSchema = useMemo(() => { @@ -298,8 +215,6 @@ const useConfig = (id: string, payload: ToolNodeType) => { setToolSettingValue, toolInputVarSchema, setInputVar, - handleOnVarOpen, - filterVar, currCollection, isShowAuthBtn, showSetAuth, @@ -307,18 +222,9 @@ const useConfig = (id: string, payload: ToolNodeType) => { hideSetAuthModal, handleSaveAuth, isLoading, - isShowSingleRun, - hideSingleRun, - inputVarValues, - varInputs, - setInputVarValues, - singleRunForms, - runningStatus, - handleRun, - handleStop, - runResult, outputSchema, hasObjectOutput, + getMoreDataForCheckValid, } } diff --git a/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts b/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts new file mode 100644 index 0000000000..a68f12fc37 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/use-get-data-for-check-more.ts @@ -0,0 +1,20 @@ +import type { ToolNodeType } from './types' +import useConfig from './use-config' + +type Params = { + id: string + payload: ToolNodeType, +} + +const useGetDataForCheckMore = ({ + id, + payload, +}: Params) => { + const { getMoreDataForCheckValid } = useConfig(id, payload) + + return { + getData: getMoreDataForCheckValid, + } +} + +export default useGetDataForCheckMore diff --git a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts new file mode 100644 index 0000000000..6fc79beebe --- /dev/null +++ b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts @@ -0,0 +1,102 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, Variable } from '@/app/components/workflow/types' +import { useCallback, useMemo, useState } from 'react' +import useNodeCrud from '../_base/hooks/use-node-crud' +import { type ToolNodeType, VarType } from './types' +import type { ValueSelector } from '@/app/components/workflow/types' +import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' +import produce from 'immer' +import type { NodeTracing } from '@/types/workflow' +import { useTranslation } from 'react-i18next' +import formatToTracingNodeList from '@/app/components/workflow/run/utils/format-log' +import { useToolIcon } from '../../hooks' + +type Params = { + id: string, + payload: ToolNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + runResult: NodeTracing +} +const useSingleRunFormParams = ({ + id, + payload, + getInputVars, + setRunInputData, + runResult, +}: Params) => { + const { t } = useTranslation() + const { inputs } = useNodeCrud<ToolNodeType>(id, payload) + + const hadVarParams = Object.keys(inputs.tool_parameters) + .filter(key => inputs.tool_parameters[key].type !== VarType.constant) + .map(k => inputs.tool_parameters[k]) + + const hadVarSettings = Object.keys(inputs.tool_configurations) + .filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant) + .map(k => inputs.tool_configurations[k]) + + const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => { + if (p.type === VarType.variable) { + // handle the old wrong value not crash the page + if (!(p.value as any).join) + return `{{#${p.value}#}}` + + return `{{#${(p.value as ValueSelector).join('.')}#}}` + } + + return p.value as string + })) + const [inputVarValues, doSetInputVarValues] = useState<Record<string, any>>({}) + const setInputVarValues = useCallback((value: Record<string, any>) => { + doSetInputVarValues(value) + setRunInputData(value) + }, [setRunInputData]) + + const inputVarValuesWithConstantValue = useCallback(() => { + const res = produce(inputVarValues, (draft) => { + Object.keys(inputs.tool_parameters).forEach((key: string) => { + const { type, value } = inputs.tool_parameters[key] + if (type === VarType.constant && (value === undefined || value === null)) { + if(!draft.tool_parameters || !draft.tool_parameters[key]) + return + draft[key] = value + } + }) + }) + return res + }, [inputs.tool_parameters, inputVarValues]) + + const forms = useMemo(() => { + const forms: FormProps[] = [{ + inputs: varInputs, + values: inputVarValuesWithConstantValue(), + onChange: setInputVarValues, + }] + return forms + }, [inputVarValuesWithConstantValue, setInputVarValues, varInputs]) + + const nodeInfo = useMemo(() => { + if (!runResult) + return null + return formatToTracingNodeList([runResult], t)[0] + }, [runResult, t]) + + const toolIcon = useToolIcon(payload) + + const getDependentVars = () => { + return varInputs.map(item => item.variable.slice(1, -1).split('.')) + } + + return { + forms, + nodeInfo, + toolIcon, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx index cf9d4152a4..60be8a0842 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx @@ -15,7 +15,7 @@ import { VarType } from '@/app/components/workflow/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { Folder } from '@/app/components/base/icons/src/vender/line/files' -import { checkKeys } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' import Toast from '@/app/components/base/toast' const i18nPrefix = 'workflow.nodes.variableAssigner' @@ -89,6 +89,7 @@ const VarGroupItem: FC<Props> = ({ }] = useBoolean(false) const handleGroupNameChange = useCallback((e: ChangeEvent<any>) => { + replaceSpaceWithUnderscreInVarNameInput(e.target) const value = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([value], false) if (!isValid) { diff --git a/web/app/components/workflow/nodes/variable-assigner/use-config.ts b/web/app/components/workflow/nodes/variable-assigner/use-config.ts index f5a7a092b3..c65941e32d 100644 --- a/web/app/components/workflow/nodes/variable-assigner/use-config.ts +++ b/web/app/components/workflow/nodes/variable-assigner/use-config.ts @@ -1,6 +1,6 @@ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import produce from 'immer' -import { useBoolean } from 'ahooks' +import { useBoolean, useDebounceFn } from 'ahooks' import { v4 as uuid4 } from 'uuid' import type { ValueSelector, Var } from '../../types' import { VarType } from '../../types' @@ -12,8 +12,13 @@ import { useNodesReadOnly, useWorkflow, } from '@/app/components/workflow/hooks' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const useConfig = (id: string, payload: VariableAssignerNodeType) => { + const { + deleteNodeInspectorVars, + renameInspectVarName, + } = useInspectVarsCrud() const { nodesReadOnly: readOnly } = useNodesReadOnly() const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow() @@ -113,7 +118,8 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { draft.advanced_settings.group_enabled = enabled }) setInputs(newInputs) - }, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm]) const handleAddGroup = useCallback(() => { let maxInGroupName = 1 @@ -134,7 +140,22 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }) }) setInputs(newInputs) - }, [inputs, setInputs]) + deleteNodeInspectorVars(id) + }, [deleteNodeInspectorVars, id, inputs, setInputs]) + + // record the first old name value + const oldNameRecord = useRef<Record<string, string>>({}) + + const { + run: renameInspectNameWithDebounce, + } = useDebounceFn( + (id: string, newName: string) => { + const oldName = oldNameRecord.current[id] + renameInspectVarName(id, oldName, newName) + delete oldNameRecord.current[id] + }, + { wait: 500 }, + ) const handleVarGroupNameChange = useCallback((groupId: string) => { return (name: string) => { @@ -144,8 +165,11 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => { }) handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[index].group_name, 'output'], [id, name, 'output']) setInputs(newInputs) + if(!(id in oldNameRecord.current)) + oldNameRecord.current[id] = inputs.advanced_settings.groups[index].group_name + renameInspectNameWithDebounce(id, name) } - }, [handleOutVarRenameChange, id, inputs, setInputs]) + }, [handleOutVarRenameChange, id, inputs, renameInspectNameWithDebounce, setInputs]) const onRemoveVarConfirm = useCallback(() => { removedVars.forEach((v) => { diff --git a/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts b/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts new file mode 100644 index 0000000000..0d6d737c21 --- /dev/null +++ b/web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts @@ -0,0 +1,92 @@ +import type { MutableRefObject } from 'react' +import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types' +import { useCallback } from 'react' +import type { VariableAssignerNodeType } from './types' + +type Params = { + id: string, + payload: VariableAssignerNodeType, + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + getInputVars: (textList: string[]) => InputVar[] + setRunInputData: (data: Record<string, any>) => void + toVarInputs: (variables: Variable[]) => InputVar[] + varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[] +} +const useSingleRunFormParams = ({ + payload, + runInputData, + setRunInputData, + varSelectorsToVarInputs, +}: Params) => { + const setInputVarValues = useCallback((newPayload: Record<string, any>) => { + setRunInputData(newPayload) + }, [setRunInputData]) + const inputVarValues = (() => { + const vars: Record<string, any> = {} + Object.keys(runInputData) + .forEach((key) => { + vars[key] = runInputData[key] + }) + return vars + })() + + const forms = (() => { + const allInputs: ValueSelector[] = [] + const isGroupEnabled = !!payload.advanced_settings?.group_enabled + if (!isGroupEnabled && payload.variables && payload.variables.length) { + payload.variables.forEach((varSelector) => { + allInputs.push(varSelector) + }) + } + if (isGroupEnabled && payload.advanced_settings && payload.advanced_settings.groups && payload.advanced_settings.groups.length) { + payload.advanced_settings.groups.forEach((group) => { + group.variables?.forEach((varSelector) => { + allInputs.push(varSelector) + }) + }) + } + + const varInputs = varSelectorsToVarInputs(allInputs) + // remove duplicate inputs + const existVarsKey: Record<string, boolean> = {} + const uniqueVarInputs: InputVar[] = [] + varInputs.forEach((input) => { + if(!input) + return + if (!existVarsKey[input.variable]) { + existVarsKey[input.variable] = true + uniqueVarInputs.push({ + ...input, + required: false, // just one of the inputs is required + }) + } + }) + return [ + { + inputs: uniqueVarInputs, + values: inputVarValues, + onChange: setInputVarValues, + }, + ] + })() + + const getDependentVars = () => { + if(payload.advanced_settings?.group_enabled) { + const vars: ValueSelector[][] = [] + payload.advanced_settings.groups.forEach((group) => { + if(group.variables) + vars.push([...group.variables]) + }) + return vars + } + return [payload.variables] + } + + return { + forms, + getDependentVars, + } +} + +export default useSingleRunFormParams diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index d35a5be8b4..5bc541a45a 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -96,7 +96,7 @@ const AddBlock = ({ onOpenChange={handleOpenChange} disabled={nodesReadOnly} onSelect={handleSelect} - placement='top-start' + placement='right-start' offset={offset ?? { mainAxis: 4, crossAxis: -8, diff --git a/web/app/components/workflow/operator/control.tsx b/web/app/components/workflow/operator/control.tsx index 5f7d19a17f..7967bf0a6c 100644 --- a/web/app/components/workflow/operator/control.tsx +++ b/web/app/components/workflow/operator/control.tsx @@ -4,6 +4,8 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { + RiAspectRatioFill, + RiAspectRatioLine, RiCursorLine, RiFunctionAddLine, RiHand, @@ -11,6 +13,7 @@ import { } from '@remixicon/react' import { useNodesReadOnly, + useWorkflowCanvasMaximize, useWorkflowMoveMode, useWorkflowOrganize, } from '../hooks' @@ -28,6 +31,7 @@ import cn from '@/utils/classnames' const Control = () => { const { t } = useTranslation() const controlMode = useStore(s => s.controlMode) + const maximizeCanvas = useStore(s => s.maximizeCanvas) const { handleModePointer, handleModeHand } = useWorkflowMoveMode() const { handleLayout } = useWorkflowOrganize() const { handleAddNote } = useOperator() @@ -35,6 +39,7 @@ const Control = () => { nodesReadOnly, getNodesReadOnly, } = useNodesReadOnly() + const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize() const addNote = (e: MouseEvent<HTMLDivElement>) => { if (getNodesReadOnly()) @@ -45,7 +50,7 @@ const Control = () => { } return ( - <div className='flex items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'> + <div className='flex flex-col items-center rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 text-text-tertiary shadow-lg'> <AddBlock /> <TipPopup title={t('workflow.nodes.note.addNote')}> <div @@ -58,7 +63,7 @@ const Control = () => { <RiStickyNoteAddLine className='h-4 w-4' /> </div> </TipPopup> - <Divider type='vertical' className='mx-0.5 h-3.5' /> + <Divider className='my-1 w-3.5' /> <TipPopup title={t('workflow.common.pointerMode')} shortcuts={['v']}> <div className={cn( @@ -83,7 +88,7 @@ const Control = () => { <RiHand className='h-4 w-4' /> </div> </TipPopup> - <Divider type='vertical' className='mx-0.5 h-3.5' /> + <Divider className='my-1 w-3.5' /> <ExportImage /> <TipPopup title={t('workflow.panel.organizeBlocks')} shortcuts={['ctrl', 'o']}> <div @@ -96,6 +101,19 @@ const Control = () => { <RiFunctionAddLine className='h-4 w-4' /> </div> </TipPopup> + <TipPopup title={maximizeCanvas ? t('workflow.panel.minimize') : t('workflow.panel.maximize')} shortcuts={['f']}> + <div + className={cn( + 'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover hover:text-text-secondary', + maximizeCanvas ? 'bg-state-accent-active text-text-accent hover:text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary', + `${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`, + )} + onClick={handleToggleMaximizeCanvas} + > + {maximizeCanvas && <RiAspectRatioFill className='h-4 w-4' />} + {!maximizeCanvas && <RiAspectRatioLine className='h-4 w-4' />} + </div> + </TipPopup> </div> ) } diff --git a/web/app/components/workflow/operator/index.tsx b/web/app/components/workflow/operator/index.tsx index 94ea8143e7..4a472a755f 100644 --- a/web/app/components/workflow/operator/index.tsx +++ b/web/app/components/workflow/operator/index.tsx @@ -1,8 +1,10 @@ -import { memo } from 'react' +import { memo, useEffect, useMemo, useRef } from 'react' import { MiniMap } from 'reactflow' import UndoRedo from '../header/undo-redo' import ZoomInOut from './zoom-in-out' -import Control from './control' +import VariableTrigger from '../variable-inspect/trigger' +import VariableInspectPanel from '../variable-inspect' +import { useStore } from '../store' export type OperatorProps = { handleUndo: () => void @@ -10,25 +12,65 @@ export type OperatorProps = { } const Operator = ({ handleUndo, handleRedo }: OperatorProps) => { + const bottomPanelRef = useRef<HTMLDivElement>(null) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const rightPanelWidth = useStore(s => s.rightPanelWidth) + const setBottomPanelWidth = useStore(s => s.setBottomPanelWidth) + const setBottomPanelHeight = useStore(s => s.setBottomPanelHeight) + + const bottomPanelWidth = useMemo(() => { + if (!workflowCanvasWidth || !rightPanelWidth) + return 'auto' + return Math.max((workflowCanvasWidth - rightPanelWidth), 400) + }, [workflowCanvasWidth, rightPanelWidth]) + + // update bottom panel height + useEffect(() => { + if (bottomPanelRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize, blockSize } = entry.borderBoxSize[0] + setBottomPanelWidth(inlineSize) + setBottomPanelHeight(blockSize) + } + }) + resizeContainerObserver.observe(bottomPanelRef.current) + return () => { + resizeContainerObserver.disconnect() + } + } + }, [setBottomPanelHeight, setBottomPanelWidth]) + return ( - <> - <MiniMap - pannable - zoomable - style={{ - width: 102, - height: 72, - }} - maskColor='var(--color-workflow-minimap-bg)' - className='!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] - !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5' - /> - <div className='absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2'> - <ZoomInOut /> + <div + ref={bottomPanelRef} + className='absolute bottom-0 left-0 right-0 z-10 px-1' + style={ + { + width: bottomPanelWidth, + } + } + > + <div className='flex justify-between px-1 pb-2'> <UndoRedo handleUndo={handleUndo} handleRedo={handleRedo} /> - <Control /> + <VariableTrigger /> + <div className='relative'> + <MiniMap + pannable + zoomable + style={{ + width: 102, + height: 72, + }} + maskColor='var(--color-workflow-minimap-bg)' + className='!absolute !bottom-10 z-[9] !m-0 !h-[73px] !w-[103px] !rounded-lg !border-[0.5px] + !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5' + /> + <ZoomInOut /> + </div> </div> - </> + <VariableInspectPanel /> + </div> ) } diff --git a/web/app/components/workflow/panel/chat-record/index.tsx b/web/app/components/workflow/panel/chat-record/index.tsx index bf8a061180..5ab3b45340 100644 --- a/web/app/components/workflow/panel/chat-record/index.tsx +++ b/web/app/components/workflow/panel/chat-record/index.tsx @@ -10,6 +10,7 @@ import { useWorkflowStore, } from '../../store' import { useWorkflowRun } from '../../hooks' +import { formatWorkflowRunIdentifier } from '../../utils' import UserInput from './user-input' import Chat from '@/app/components/base/chat/chat' import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types' @@ -99,7 +100,7 @@ const ChatRecord = () => { {fetched && ( <> <div className='flex shrink-0 items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'> - {`TEST CHAT#${historyWorkflowData?.sequence_number}`} + {`TEST CHAT${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`} <div className='flex h-6 w-6 cursor-pointer items-center justify-center' onClick={() => { @@ -114,6 +115,7 @@ const ChatRecord = () => { <Chat config={{ supportCitationHitInfo: true, + questionEditEnable: false, } as any} chatList={threadChatItems} chatContainerClassName='px-3' diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index d8da0e69a3..869317ca6a 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -16,7 +16,7 @@ import type { ConversationVariable } from '@/app/components/workflow/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import cn from '@/utils/classnames' -import { checkKeys } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' export type ModalPropsType = { chatVar?: ConversationVariable @@ -80,7 +80,7 @@ const ChatVariableModal = ({ const [objectValue, setObjectValue] = React.useState<ObjectValueItem[]>([DEFAULT_OBJECT_VALUE]) const [editorContent, setEditorContent] = React.useState<string>() const [editInJSON, setEditInJSON] = React.useState(false) - const [des, setDes] = React.useState<string>('') + const [description, setDescription] = React.useState<string>('') const editorMinHeight = useMemo(() => { if (type === ChatVarType.ArrayObject) @@ -143,6 +143,13 @@ const ChatVariableModal = ({ return true } + const handleVarNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { + replaceSpaceWithUnderscreInVarNameInput(e.target) + if (!!e.target.value && !checkVariableName(e.target.value)) + return + setName(e.target.value || '') + } + const handleTypeChange = (v: ChatVarType) => { setValue(undefined) setEditorContent(undefined) @@ -230,7 +237,7 @@ const ChatVariableModal = ({ name, value_type: type, value: formatValue(value), - description: des, + description, }) onClose() } @@ -240,7 +247,7 @@ const ChatVariableModal = ({ setName(chatVar.name) setType(chatVar.value_type) setValue(chatVar.value) - setDes(chatVar.description) + setDescription(chatVar.description) setObjectValue(getObjectValue()) if (chatVar.value_type === ChatVarType.ArrayObject) { setEditorContent(JSON.stringify(chatVar.value)) @@ -275,7 +282,7 @@ const ChatVariableModal = ({ <Input placeholder={t('workflow.chatVariable.modal.namePlaceholder') || ''} value={name} - onChange={e => setName(e.target.value || '')} + onChange={handleVarNameChange} onBlur={e => checkVariableName(e.target.value)} type='text' /> @@ -324,7 +331,7 @@ const ChatVariableModal = ({ {type === ChatVarType.String && ( // Input will remove \n\r, so use Textarea just like description area <textarea - className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' + className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' value={value} placeholder={t('workflow.chatVariable.modal.valuePlaceholder') || ''} onChange={e => setValue(e.target.value)} @@ -377,10 +384,10 @@ const ChatVariableModal = ({ <div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.chatVariable.modal.description')}</div> <div className='flex'> <textarea - className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' - value={des} + className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' + value={description} placeholder={t('workflow.chatVariable.modal.descriptionPlaceholder') || ''} - onChange={e => setDes(e.target.value)} + onChange={e => setDescription(e.target.value)} /> </div> </div> diff --git a/web/app/components/workflow/panel/chat-variable-panel/index.tsx b/web/app/components/workflow/panel/chat-variable-panel/index.tsx index ad00bddd0c..7006c18cc5 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/index.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/index.tsx @@ -3,7 +3,6 @@ import { useCallback, useState, } from 'react' -import { useContext } from 'use-context-selector' import { useStoreApi, } from 'reactflow' @@ -22,18 +21,28 @@ import type { import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' import { BlockEnum } from '@/app/components/workflow/types' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' import cn from '@/utils/classnames' +import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' const ChatVariablePanel = () => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const store = useStoreApi() const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel) const varList = useStore(s => s.conversationVariables) as ConversationVariable[] const updateChatVarList = useStore(s => s.setConversationVariables) const { doSyncWorkflowDraft } = useNodesSyncDraft() + const { + invalidateConversationVarValues, + } = useInspectVarsCrud() + const handleVarChanged = useCallback(() => { + doSyncWorkflowDraft(false, { + onSuccess() { + invalidateConversationVarValues() + }, + }) + }, [doSyncWorkflowDraft, invalidateConversationVarValues]) const [showTip, setShowTip] = useState(true) const [showVariableModal, setShowVariableModal] = useState(false) @@ -73,8 +82,8 @@ const ChatVariablePanel = () => { updateChatVarList(varList.filter(v => v.id !== chatVar.id)) setCacheForDelete(undefined) setShowRemoveConfirm(false) - doSyncWorkflowDraft() - }, [doSyncWorkflowDraft, removeUsedVarInNodes, updateChatVarList, varList]) + handleVarChanged() + }, [handleVarChanged, removeUsedVarInNodes, updateChatVarList, varList]) const deleteCheck = useCallback((chatVar: ConversationVariable) => { const effectedNodes = getEffectedNodes(chatVar) @@ -92,7 +101,7 @@ const ChatVariablePanel = () => { if (!currentVar) { const newList = [chatVar, ...varList] updateChatVarList(newList) - doSyncWorkflowDraft() + handleVarChanged() return } // edit chatVar @@ -110,8 +119,8 @@ const ChatVariablePanel = () => { }) setNodes(newNodes) } - doSyncWorkflowDraft() - }, [currentVar, doSyncWorkflowDraft, getEffectedNodes, store, updateChatVarList, varList]) + handleVarChanged() + }, [currentVar, getEffectedNodes, handleVarChanged, store, updateChatVarList, varList]) return ( <div @@ -139,7 +148,13 @@ const ChatVariablePanel = () => { <div className='system-2xs-medium-uppercase inline-block rounded-[5px] border border-divider-deep px-[5px] py-[3px] text-text-tertiary'>TIPS</div> <div className='system-sm-regular mb-4 mt-1 text-text-secondary'> {t('workflow.chatVariable.panelDescription')} - <a target='_blank' rel='noopener noreferrer' className='text-text-accent' href={locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/guides/workflow/variables#conversation-variables' : `https://docs.dify.ai/${locale.toLowerCase()}/guides/workflow/variables#hui-hua-bian-liang`}>{t('workflow.chatVariable.docLink')}</a> + <a target='_blank' rel='noopener noreferrer' className='text-text-accent' + href={docLink('/guides/workflow/variables#conversation-variables', { + 'zh-Hans': '/guides/workflow/variables#会话变量', + 'ja-JP': '/guides/workflow/variables#会話変数', + })}> + {t('workflow.chatVariable.docLink')} + </a> </div> <div className='flex items-center gap-2'> <div className='radius-lg flex flex-col border border-workflow-block-border bg-workflow-block-bg p-3 pb-4 shadow-md'> @@ -166,7 +181,7 @@ const ChatVariablePanel = () => { </div> </div> </div> - <div className='absolute right-[38px] top-[-4px] z-10 h-3 w-3 rotate-45 bg-background-section-burn'/> + <div className='absolute right-[38px] top-[-4px] z-10 h-3 w-3 rotate-45 bg-background-section-burn' /> </div> </div> )} diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index 8f45bc5774..47d2c73a14 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -21,6 +21,8 @@ import { import { useStore as useAppStore } from '@/app/components/app/store' import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils' import type { FileEntity } from '@/app/components/base/file-uploader/types' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' type ChatWrapperProps = { showConversationVariableModal: boolean @@ -105,6 +107,12 @@ const ChatWrapper = ( ) }, [chatList, doSend]) + const { eventEmitter } = useEventEmitterContextContext() + eventEmitter?.useSubscription((v: any) => { + if (v.type === EVENT_WORKFLOW_STOP) + handleStop() + }) + useImperativeHandle(ref, () => { return { handleRestart, diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 4ef33a6bc6..d98cf49812 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -8,7 +8,10 @@ import { import { useTranslation } from 'react-i18next' import { produce, setAutoFreeze } from 'immer' import { uniqBy } from 'lodash-es' -import { useWorkflowRun } from '../../hooks' +import { + useSetWorkflowVarsWithValue, + useWorkflowRun, +} from '../../hooks' import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' import { useWorkflowStore } from '../../store' import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../constants' @@ -30,6 +33,8 @@ import { } from '@/app/components/base/file-uploader/utils' import type { FileEntity } from '@/app/components/base/file-uploader/types' import { getThreadMessages } from '@/app/components/base/chat/utils' +import { useInvalidAllLastRun } from '@/service/use-workflow' +import { useParams } from 'next/navigation' type GetAbortController = (abortController: AbortController) => void type SendCallback = { @@ -53,6 +58,9 @@ export const useChat = ( const taskIdRef = useRef('') const [isResponding, setIsResponding] = useState(false) const isRespondingRef = useRef(false) + const { appId } = useParams() + const invalidAllLastRun = useInvalidAllLastRun(appId as string) + const { fetchInspectVars } = useSetWorkflowVarsWithValue() const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) const { @@ -84,7 +92,7 @@ export const useChat = ( ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), } } else { @@ -93,7 +101,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), }) } } @@ -288,6 +296,8 @@ export const useChat = ( }, async onCompleted(hasError?: boolean, errorMessage?: string) { handleResponding(false) + fetchInspectVars() + invalidAllLastRun() if (hasError) { if (errorMessage) { @@ -491,7 +501,7 @@ export const useChat = ( }, }, ) - }, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled]) + }, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled, fetchInspectVars, invalidAllLastRun]) return { conversationId: conversationId.current, diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 563be25a99..ff09f48625 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, - useEffect, + useMemo, useRef, useState, } from 'react' @@ -16,14 +16,14 @@ import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { BlockEnum } from '../../types' import type { StartNodeType } from '../../nodes/start/types' +import { useResizePanel } from '../../nodes/_base/hooks/use-resize-panel' import ChatWrapper from './chat-wrapper' import cn from '@/utils/classnames' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' -import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import Tooltip from '@/app/components/base/tooltip' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import { useStore } from '@/app/components/workflow/store' -import { noop } from 'lodash-es' +import { debounce, noop } from 'lodash-es' export type ChatWrapperRefType = { handleRestart: () => void @@ -34,9 +34,9 @@ const DebugAndPreview = () => { const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync() const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() - const varList = useStore(s => s.conversationVariables) const [expanded, setExpanded] = useState(true) const nodes = useNodes<StartNodeType>() + const selectedNode = nodes.find(node => node.data.selected) const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const variables = startNode?.data.variables || [] const visibleVariables = variables.filter(v => v.hide !== true) @@ -49,94 +49,88 @@ const DebugAndPreview = () => { chatRef.current.handleRestart() } - const [panelWidth, setPanelWidth] = useState(420) - const [isResizing, setIsResizing] = useState(false) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const panelWidth = useStore(s => s.previewPanelWidth) + const setPanelWidth = useStore(s => s.setPreviewPanelWidth) + const handleResize = useCallback((width: number) => { + localStorage.setItem('debug-and-preview-panel-width', `${width}`) + setPanelWidth(width) + }, [setPanelWidth]) + const maxPanelWidth = useMemo(() => { + if (!workflowCanvasWidth) + return 720 - const startResizing = useCallback((e: React.MouseEvent) => { - e.preventDefault() - setIsResizing(true) - }, []) + if (!selectedNode) + return workflowCanvasWidth - 400 - const stopResizing = useCallback(() => { - setIsResizing(false) - }, []) - - const resize = useCallback((e: MouseEvent) => { - if (isResizing) { - const newWidth = window.innerWidth - e.clientX - if (newWidth > 420 && newWidth < 1024) - setPanelWidth(newWidth) - } - }, [isResizing]) - - useEffect(() => { - window.addEventListener('mousemove', resize) - window.addEventListener('mouseup', stopResizing) - return () => { - window.removeEventListener('mousemove', resize) - window.removeEventListener('mouseup', stopResizing) - } - }, [resize, stopResizing]) + return workflowCanvasWidth - 400 - 400 + }, [workflowCanvasWidth, selectedNode, nodePanelWidth]) + const { + triggerRef, + containerRef, + } = useResizePanel({ + direction: 'horizontal', + triggerDirection: 'left', + minWidth: 400, + maxWidth: maxPanelWidth, + onResize: debounce(handleResize), + }) return ( - <div - className={cn( - 'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl', - )} - style={{ width: `${panelWidth}px` }} - > + <div className='relative h-full'> + <div + ref={triggerRef} + className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'> + <div className='h-10 w-0.5 rounded-sm bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid'></div> + </div> <div - className="absolute bottom-0 left-[3px] top-1/2 z-50 h-6 w-[3px] cursor-col-resize rounded bg-gray-300" - onMouseDown={startResizing} - /> - <div className='system-xl-semibold flex shrink-0 items-center justify-between px-4 pb-2 pt-3 text-text-primary'> - <div className='h-8'>{t('workflow.common.debugAndPreview').toLocaleUpperCase()}</div> - <div className='flex items-center gap-1'> - <Tooltip - popupContent={t('common.operation.refresh')} - > - <ActionButton onClick={() => handleRestartChat()}> - <RefreshCcw01 className='h-4 w-4' /> - </ActionButton> - </Tooltip> - {varList.length > 0 && ( + ref={containerRef} + className={cn( + 'relative flex h-full flex-col rounded-l-2xl border border-r-0 border-components-panel-border bg-chatbot-bg shadow-xl', + )} + style={{ width: `${panelWidth}px` }} + > + <div className='system-xl-semibold flex shrink-0 items-center justify-between px-4 pb-2 pt-3 text-text-primary'> + <div className='h-8'>{t('workflow.common.debugAndPreview').toLocaleUpperCase()}</div> + <div className='flex items-center gap-1'> <Tooltip - popupContent={t('workflow.chatVariable.panelTitle')} + popupContent={t('common.operation.refresh')} > - <ActionButton onClick={() => setShowConversationVariableModal(true)}> - <BubbleX className='h-4 w-4' /> + <ActionButton onClick={() => handleRestartChat()}> + <RefreshCcw01 className='h-4 w-4' /> </ActionButton> </Tooltip> - )} - {visibleVariables.length > 0 && ( - <div className='relative'> - <Tooltip - popupContent={t('workflow.panel.userInputField')} - > - <ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}> - <RiEqualizer2Line className='h-4 w-4' /> - </ActionButton> - </Tooltip> - {expanded && <div className='absolute bottom-[-17px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg' />} + {visibleVariables.length > 0 && ( + <div className='relative'> + <Tooltip + popupContent={t('workflow.panel.userInputField')} + > + <ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}> + <RiEqualizer2Line className='h-4 w-4' /> + </ActionButton> + </Tooltip> + {expanded && <div className='absolute bottom-[-17px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg' />} + </div> + )} + <div className='mx-3 h-3.5 w-[1px] bg-divider-regular'></div> + <div + className='flex h-6 w-6 cursor-pointer items-center justify-center' + onClick={handleCancelDebugAndPreviewPanel} + > + <RiCloseLine className='h-4 w-4 text-text-tertiary' /> </div> - )} - <div className='mx-3 h-3.5 w-[1px] bg-divider-regular'></div> - <div - className='flex h-6 w-6 cursor-pointer items-center justify-center' - onClick={handleCancelDebugAndPreviewPanel} - > - <RiCloseLine className='h-4 w-4 text-text-tertiary' /> </div> </div> - </div> - <div className='grow overflow-y-auto rounded-b-2xl'> - <ChatWrapper - ref={chatRef} - showConversationVariableModal={showConversationVariableModal} - onConversationModalHide={() => setShowConversationVariableModal(false)} - showInputsFieldsPanel={expanded} - onHide={() => setExpanded(false)} - /> + <div className='grow overflow-y-auto rounded-b-2xl'> + <ChatWrapper + ref={chatRef} + showConversationVariableModal={showConversationVariableModal} + onConversationModalHide={() => setShowConversationVariableModal(false)} + showInputsFieldsPanel={expanded} + onHide={() => setExpanded(false)} + /> + </div> </div> </div> ) diff --git a/web/app/components/workflow/panel/env-panel/env-item.tsx b/web/app/components/workflow/panel/env-panel/env-item.tsx index 91abafa8f6..33ed45cb67 100644 --- a/web/app/components/workflow/panel/env-panel/env-item.tsx +++ b/web/app/components/workflow/panel/env-panel/env-item.tsx @@ -22,30 +22,42 @@ const EnvItem = ({ return ( <div className={cn( - 'radius-md mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 py-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', + 'radius-md group mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs hover:bg-components-panel-on-panel-item-bg-hover', destructive && 'border-state-destructive-border hover:bg-state-destructive-hover', )}> - <div className='flex items-center justify-between'> - <div className='flex grow items-center gap-1'> - <Env className='h-4 w-4 text-util-colors-violet-violet-600' /> - <div className='system-sm-medium text-text-primary'>{env.name}</div> - <div className='system-xs-medium text-text-tertiary'>{capitalize(env.value_type)}</div> - {env.value_type === 'secret' && <RiLock2Line className='h-3 w-3 text-text-tertiary' />} - </div> - <div className='flex shrink-0 items-center gap-1 text-text-tertiary'> - <div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'> - <RiEditLine className='h-4 w-4' onClick={() => onEdit(env)}/> + <div className='px-2.5 py-2'> + <div className='flex items-center justify-between'> + <div className='flex grow items-center gap-1'> + <Env className='h-4 w-4 text-util-colors-violet-violet-600' /> + <div className='system-sm-medium text-text-primary'>{env.name}</div> + <div className='system-xs-medium text-text-tertiary'>{capitalize(env.value_type)}</div> + {env.value_type === 'secret' && <RiLock2Line className='h-3 w-3 text-text-tertiary' />} </div> - <div - className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive' - onMouseOver={() => setDestructive(true)} - onMouseOut={() => setDestructive(false)} - > - <RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(env)} /> + <div className='flex shrink-0 items-center gap-1 text-text-tertiary'> + <div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'> + <RiEditLine className='h-4 w-4' onClick={() => onEdit(env)}/> + </div> + <div + className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive' + onMouseOver={() => setDestructive(true)} + onMouseOut={() => setDestructive(false)} + > + <RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(env)} /> + </div> </div> </div> + <div className='system-xs-regular truncate text-text-tertiary'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div> </div> - <div className='system-xs-regular truncate text-text-tertiary'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div> + {env.description && ( + <> + <div className={'h-[0.5px] bg-divider-subtle'} /> + <div className={cn('rounded-bl-[8px] rounded-br-[8px] bg-background-default-subtle px-2.5 py-2 group-hover:bg-transparent', + destructive && 'bg-state-destructive-hover hover:bg-state-destructive-hover', + )}> + <div className='system-xs-regular truncate text-text-tertiary'>{env.description}</div> + </div> + </> + )} </div> ) } diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index 4546aabae6..4877575d7e 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -10,7 +10,7 @@ import { ToastContext } from '@/app/components/base/toast' import { useStore } from '@/app/components/workflow/store' import type { EnvironmentVariable } from '@/app/components/workflow/types' import cn from '@/utils/classnames' -import { checkKeys } from '@/utils/var' +import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' export type ModalPropsType = { env?: EnvironmentVariable @@ -29,6 +29,7 @@ const VariableModal = ({ const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string') const [name, setName] = React.useState('') const [value, setValue] = React.useState<any>() + const [description, setDescription] = React.useState<string>('') const checkVariableName = (value: string) => { const { isValid, errorMessageKey } = checkKeys([value], false) @@ -42,6 +43,13 @@ const VariableModal = ({ return true } + const handleVarNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { + replaceSpaceWithUnderscreInVarNameInput(e.target) + if (!!e.target.value && !checkVariableName(e.target.value)) + return + setName(e.target.value || '') + } + const handleSave = () => { if (!checkVariableName(name)) return @@ -60,6 +68,7 @@ const VariableModal = ({ value_type: type, name, value: type === 'number' ? Number(value) : value, + description, }) onClose() } @@ -69,6 +78,7 @@ const VariableModal = ({ setType(env.value_type) setName(env.name) setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value) + setDescription(env.description) } }, [env, envSecrets]) @@ -127,19 +137,19 @@ const VariableModal = ({ <Input placeholder={t('workflow.env.modal.namePlaceholder') || ''} value={name} - onChange={e => setName(e.target.value || '')} + onChange={handleVarNameChange} onBlur={e => checkVariableName(e.target.value)} type='text' /> </div> </div> {/* value */} - <div className=''> + <div className='mb-4'> <div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.value')}</div> <div className='flex'> { type !== 'number' ? <textarea - className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' + className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' value={value} placeholder={t('workflow.env.modal.valuePlaceholder') || ''} onChange={e => setValue(e.target.value)} @@ -153,6 +163,18 @@ const VariableModal = ({ } </div> </div> + {/* description */} + <div className=''> + <div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.description')}</div> + <div className='flex'> + <textarea + className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs' + value={description} + placeholder={t('workflow.env.modal.descriptionPlaceholder') || ''} + onChange={e => setDescription(e.target.value)} + /> + </div> + </div> </div> <div className='flex flex-row-reverse rounded-b-2xl p-4 pt-2'> <div className='flex gap-2'> diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 8e510f4e77..ccfe8fac58 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' -import { memo } from 'react' -import { useNodes } from 'reactflow' -import type { CommonNodeType } from '../types' +import { useShallow } from 'zustand/react/shallow' +import { memo, useCallback, useEffect, useRef } from 'react' +import { useStore as useReactflow } from 'reactflow' import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import EnvPanel from './env-panel' @@ -13,36 +13,123 @@ export type PanelProps = { right?: React.ReactNode } } + +/** + * Reference MDN standard implementation:https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserverEntry/borderBoxSize + */ +const getEntryWidth = (entry: ResizeObserverEntry, element: HTMLElement): number => { + if (entry.borderBoxSize?.length > 0) + return entry.borderBoxSize[0].inlineSize + + if (entry.contentRect.width > 0) + return entry.contentRect.width + + return element.getBoundingClientRect().width +} + +const useResizeObserver = ( + callback: (width: number) => void, + dependencies: React.DependencyList = [], +) => { + const elementRef = useRef<HTMLDivElement>(null) + + const stableCallback = useCallback(callback, [callback]) + + useEffect(() => { + const element = elementRef.current + if (!element) return + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = getEntryWidth(entry, element) + stableCallback(width) + } + }) + + resizeObserver.observe(element) + + const initialWidth = element.getBoundingClientRect().width + stableCallback(initialWidth) + + return () => { + resizeObserver.disconnect() + } + }, [stableCallback, ...dependencies]) + return elementRef +} + const Panel: FC<PanelProps> = ({ components, }) => { - const nodes = useNodes<CommonNodeType>() - const selectedNode = nodes.find(node => node.data.selected) + const selectedNode = useReactflow(useShallow((s) => { + const nodes = s.getNodes() + const currentNode = nodes.find(node => node.data.selected) + + if (currentNode) { + return { + id: currentNode.id, + type: currentNode.type, + data: currentNode.data, + } + } + })) const showEnvPanel = useStore(s => s.showEnvPanel) const isRestoring = useStore(s => s.isRestoring) + const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel) + + // widths used for adaptive layout + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const previewPanelWidth = useStore(s => s.previewPanelWidth) + const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth) + + // When a node is selected and the NodePanel appears, if the current width + // of preview/otherPanel is too large, it may result in the total width of + // the two panels exceeding the workflowCanvasWidth, causing the NodePanel + // to be pushed out. Here we check and, if necessary, reduce the previewPanelWidth + // to "workflowCanvasWidth - 400 (minimum NodePanel width) - 400 (minimum canvas space)", + // while still ensuring that previewPanelWidth ≥ 400. + + useEffect(() => { + if (!selectedNode || !workflowCanvasWidth) + return + + const reservedCanvasWidth = 400 // Reserve the minimum visible width for the canvas + const minNodePanelWidth = 400 + const maxAllowed = Math.max(workflowCanvasWidth - reservedCanvasWidth - minNodePanelWidth, 400) + + if (previewPanelWidth > maxAllowed) + setPreviewPanelWidth(maxAllowed) + }, [selectedNode, workflowCanvasWidth, previewPanelWidth, setPreviewPanelWidth]) + + const setRightPanelWidth = useStore(s => s.setRightPanelWidth) + const setOtherPanelWidth = useStore(s => s.setOtherPanelWidth) + + const rightPanelRef = useResizeObserver( + setRightPanelWidth, + [setRightPanelWidth, selectedNode, showEnvPanel, showWorkflowVersionHistoryPanel], + ) + + const otherPanelRef = useResizeObserver( + setOtherPanelWidth, + [setOtherPanelWidth, showEnvPanel, showWorkflowVersionHistoryPanel], + ) return ( <div + ref={rightPanelRef} tabIndex={-1} - className={cn('absolute bottom-2 right-0 top-14 z-10 flex outline-none')} + className={cn('absolute bottom-1 right-0 top-14 z-10 flex outline-none')} key={`${isRestoring}`} > - { - components?.left - } - { - !!selectedNode && ( - <NodePanel {...selectedNode!} /> - ) - } - { - components?.right - } - { - showEnvPanel && ( - <EnvPanel /> - ) - } + {components?.left} + {!!selectedNode && <NodePanel {...selectedNode} />} + <div + className="relative" + ref={otherPanelRef} + > + {components?.right} + {showEnvPanel && <EnvPanel />} + </div> </div> ) } diff --git a/web/app/components/workflow/panel/record.tsx b/web/app/components/workflow/panel/record.tsx index 70fe9c44e0..534bc633a6 100644 --- a/web/app/components/workflow/panel/record.tsx +++ b/web/app/components/workflow/panel/record.tsx @@ -3,6 +3,7 @@ import type { WorkflowDataUpdater } from '../types' import Run from '../run' import { useStore } from '../store' import { useWorkflowUpdate } from '../hooks' +import { formatWorkflowRunIdentifier } from '../utils' const Record = () => { const historyWorkflowData = useStore(s => s.historyWorkflowData) @@ -20,7 +21,7 @@ const Record = () => { return ( <div className='flex h-full w-[400px] flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'> <div className='system-xl-semibold flex items-center justify-between p-4 pb-0 text-text-primary'> - {`Test Run#${historyWorkflowData?.sequence_number}`} + {`Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`} </div> <Run runID={historyWorkflowData?.id || ''} diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index a42eb0f215..521ab58780 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -9,7 +9,7 @@ import VersionHistoryItem from './version-history-item' import Filter from './filter' import type { VersionHistory } from '@/types/workflow' import { useStore as useAppStore } from '@/app/components/app/store' -import { useDeleteWorkflow, useResetWorkflowVersionHistory, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow' +import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow' import Divider from '@/app/components/base/divider' import Loading from './loading' import Empty from './empty' @@ -37,6 +37,10 @@ const VersionHistoryPanel = () => { const currentVersion = useStore(s => s.currentVersion) const setCurrentVersion = useStore(s => s.setCurrentVersion) const userProfile = useAppContextSelector(s => s.userProfile) + const invalidAllLastRun = useInvalidAllLastRun(appDetail!.id) + const { + deleteAllInspectVars, + } = workflowStore.getState() const { t } = useTranslation() const { @@ -125,6 +129,8 @@ const VersionHistoryPanel = () => { type: 'success', message: t('workflow.versionHistory.action.restoreSuccess'), }) + deleteAllInspectVars() + invalidAllLastRun() }, onError: () => { Toast.notify({ @@ -136,7 +142,7 @@ const VersionHistoryPanel = () => { resetWorkflowVersionHistory() }, }) - }, [setShowWorkflowVersionHistoryPanel, handleSyncWorkflowDraft, workflowStore, handleRestoreFromPublishedWorkflow, resetWorkflowVersionHistory, t]) + }, [setShowWorkflowVersionHistoryPanel, handleRestoreFromPublishedWorkflow, workflowStore, handleSyncWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, resetWorkflowVersionHistory]) const { mutateAsync: deleteWorkflow } = useDeleteWorkflow(appDetail!.id) @@ -149,6 +155,8 @@ const VersionHistoryPanel = () => { message: t('workflow.versionHistory.action.deleteSuccess'), }) resetWorkflowVersionHistory() + deleteAllInspectVars() + invalidAllLastRun() }, onError: () => { Toast.notify({ @@ -160,7 +168,7 @@ const VersionHistoryPanel = () => { setDeleteConfirmOpen(false) }, }) - }, [t, deleteWorkflow, resetWorkflowVersionHistory]) + }, [deleteWorkflow, t, resetWorkflowVersionHistory, deleteAllInspectVars, invalidAllLastRun]) const { mutateAsync: updateWorkflow } = useUpdateWorkflow(appDetail!.id) diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index 34b0ec6395..2c797e05d6 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -20,6 +20,7 @@ import { useStore } from '../store' import { WorkflowRunningStatus, } from '../types' +import { formatWorkflowRunIdentifier } from '../utils' import Toast from '../../base/toast' import InputsPanel from './inputs-panel' import cn from '@/utils/classnames' @@ -31,6 +32,9 @@ const WorkflowPreview = () => { const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() const workflowRunningData = useStore(s => s.workflowRunningData) const showInputsPanel = useStore(s => s.showInputsPanel) + const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) + const panelWidth = useStore(s => s.previewPanelWidth) + const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth) const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) const [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING') @@ -48,7 +52,6 @@ const WorkflowPreview = () => { switchTab('DETAIL') }, [workflowRunningData]) - const [panelWidth, setPanelWidth] = useState(420) const [isResizing, setIsResizing] = useState(false) const startResizing = useCallback((e: React.MouseEvent) => { @@ -63,10 +66,14 @@ const WorkflowPreview = () => { const resize = useCallback((e: MouseEvent) => { if (isResizing) { const newWidth = window.innerWidth - e.clientX - if (newWidth > 420 && newWidth < 1024) - setPanelWidth(newWidth) + // width constraints: 400 <= width <= maxAllowed (canvas - reserved 400) + const reservedCanvasWidth = 400 + const maxAllowed = workflowCanvasWidth ? (workflowCanvasWidth - reservedCanvasWidth) : 1024 + + if (newWidth >= 400 && newWidth <= maxAllowed) + setPreviewPanelWidth(newWidth) } - }, [isResizing]) + }, [isResizing, workflowCanvasWidth, setPreviewPanelWidth]) useEffect(() => { window.addEventListener('mousemove', resize) @@ -78,9 +85,9 @@ const WorkflowPreview = () => { }, [resize, stopResizing]) return ( - <div className={` - relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl - `} + <div className={ + 'relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl' + } style={{ width: `${panelWidth}px` }} > <div @@ -88,7 +95,7 @@ const WorkflowPreview = () => { onMouseDown={startResizing} /> <div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'> - {`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`} + {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at)}`} <div className='cursor-pointer p-1' onClick={() => handleCancelDebugAndPreviewPanel()}> <RiCloseLine className='h-4 w-4 text-text-tertiary' /> </div> diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index 10f641cc8f..a4df5f4c74 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -28,6 +28,8 @@ import type { } from '@/types/workflow' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' import { hasRetryNode } from '@/app/components/workflow/utils' +import { useDocLink } from '@/context/i18n' +import Tooltip from '@/app/components/base/tooltip' type Props = { className?: string @@ -65,6 +67,7 @@ const NodePanel: FC<Props> = ({ doSetCollapseState(state) }, [hideProcessDetail]) const { t } = useTranslation() + const docLink = useDocLink() const getTime = (time: number) => { if (time < 1) @@ -127,10 +130,16 @@ const NodePanel: FC<Props> = ({ /> )} <BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('mr-2 shrink-0', inMessage && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} /> - <div className={cn( - 'system-xs-semibold-uppercase grow truncate text-text-secondary', - hideInfo && '!text-xs', - )} title={nodeInfo.title}>{nodeInfo.title}</div> + <Tooltip + popupContent={ + <div className='max-w-xs'>{nodeInfo.title}</div> + } + > + <div className={cn( + 'system-xs-semibold-uppercase grow truncate text-text-secondary', + hideInfo && '!text-xs', + )}>{nodeInfo.title}</div> + </Tooltip> {nodeInfo.status !== 'running' && !hideInfo && ( <div className='system-xs-regular shrink-0 text-text-tertiary'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div> )} @@ -195,7 +204,7 @@ const NodePanel: FC<Props> = ({ <StatusContainer status='stopped'> {nodeInfo.error} <a - href='https://docs.dify.ai/guides/workflow/error-handling/error-type' + href={docLink('/guides/workflow/error-handling/error-type')} target='_blank' className='text-text-accent' > diff --git a/web/app/components/workflow/run/result-panel.tsx b/web/app/components/workflow/run/result-panel.tsx index 096a825bc6..14260e646b 100644 --- a/web/app/components/workflow/run/result-panel.tsx +++ b/web/app/components/workflow/run/result-panel.tsx @@ -17,11 +17,11 @@ import { LoopLogTrigger } from '@/app/components/workflow/run/loop-log' import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log' import { AgentLogTrigger } from '@/app/components/workflow/run/agent-log' -type ResultPanelProps = { +export type ResultPanelProps = { nodeInfo?: NodeTracing inputs?: string process_data?: string - outputs?: string + outputs?: string | Record<string, any> status: string error?: string elapsed_time?: number diff --git a/web/app/components/workflow/run/status.tsx b/web/app/components/workflow/run/status.tsx index 2f870f2e06..253aaab3a1 100644 --- a/web/app/components/workflow/run/status.tsx +++ b/web/app/components/workflow/run/status.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import cn from '@/utils/classnames' import Indicator from '@/app/components/header/indicator' import StatusContainer from '@/app/components/workflow/run/status-container' +import { useDocLink } from '@/context/i18n' type ResultProps = { status: string @@ -21,6 +22,7 @@ const StatusPanel: FC<ResultProps> = ({ exceptionCounts, }) => { const { t } = useTranslation() + const docLink = useDocLink() return ( <StatusContainer status={status}> @@ -134,7 +136,7 @@ const StatusPanel: FC<ResultProps> = ({ <div className='system-xs-medium text-text-warning'> {error} <a - href='https://docs.dify.ai/guides/workflow/error-handling/error-type' + href={docLink('/guides/workflow/error-handling/error-type')} target='_blank' className='text-text-accent' > diff --git a/web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts b/web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts new file mode 100644 index 0000000000..51f66bee13 --- /dev/null +++ b/web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts @@ -0,0 +1,142 @@ +import type { StateCreator } from 'zustand' +import produce from 'immer' +import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import type { ValueSelector } from '../../../types' + +type InspectVarsState = { + currentFocusNodeId: string | null + nodesWithInspectVars: NodeWithVar[] // the nodes have data + conversationVars: VarInInspect[] +} + +type InspectVarsActions = { + setCurrentFocusNodeId: (nodeId: string | null) => void + setNodesWithInspectVars: (payload: NodeWithVar[]) => void + deleteAllInspectVars: () => void + setNodeInspectVars: (nodeId: string, payload: VarInInspect[]) => void + deleteNodeInspectVars: (nodeId: string) => void + setInspectVarValue: (nodeId: string, name: string, value: any) => void + resetToLastRunVar: (nodeId: string, varId: string, value: any) => void + renameInspectVarName: (nodeId: string, varId: string, selector: ValueSelector) => void + deleteInspectVar: (nodeId: string, varId: string) => void +} + +export type InspectVarsSliceShape = InspectVarsState & InspectVarsActions + +export const createInspectVarsSlice: StateCreator<InspectVarsSliceShape> = (set, get) => { + return ({ + currentFocusNodeId: null, + nodesWithInspectVars: [], + conversationVars: [], + setCurrentFocusNodeId: (nodeId) => { + set(() => ({ + currentFocusNodeId: nodeId, + })) + }, + setNodesWithInspectVars: (payload) => { + set(() => ({ + nodesWithInspectVars: payload, + })) + }, + deleteAllInspectVars: () => { + set(() => ({ + nodesWithInspectVars: [], + })) + }, + setNodeInspectVars: (nodeId, payload) => { + set((state) => { + const prevNodes = state.nodesWithInspectVars + const nodes = produce(prevNodes, (draft) => { + const index = prevNodes.findIndex(node => node.nodeId === nodeId) + if (index !== -1) { + draft[index].vars = payload + draft[index].isValueFetched = true + } + }) + + return { + nodesWithInspectVars: nodes, + } + }) + }, + deleteNodeInspectVars: (nodeId) => { + set((state: InspectVarsSliceShape) => { + const nodes = state.nodesWithInspectVars.filter(node => node.nodeId !== nodeId) + return { + nodesWithInspectVars: nodes, + } + }, + ) + }, + setInspectVarValue: (nodeId, varId, value) => { + set((state: InspectVarsSliceShape) => { + const nodes = produce(state.nodesWithInspectVars, (draft) => { + const targetNode = draft.find(node => node.nodeId === nodeId) + if (!targetNode) + return + const targetVar = targetNode.vars.find(varItem => varItem.id === varId) + if(!targetVar) + return + targetVar.value = value + targetVar.edited = true + }, + ) + return { + nodesWithInspectVars: nodes, + } + }) + }, + resetToLastRunVar: (nodeId, varId, value) => { + set((state: InspectVarsSliceShape) => { + const nodes = produce(state.nodesWithInspectVars, (draft) => { + const targetNode = draft.find(node => node.nodeId === nodeId) + if (!targetNode) + return + const targetVar = targetNode.vars.find(varItem => varItem.id === varId) + if(!targetVar) + return + targetVar.value = value + targetVar.edited = false + }, + ) + return { + nodesWithInspectVars: nodes, + } + }) + }, + renameInspectVarName: (nodeId, varId, selector) => { + set((state: InspectVarsSliceShape) => { + const nodes = produce(state.nodesWithInspectVars, (draft) => { + const targetNode = draft.find(node => node.nodeId === nodeId) + if (!targetNode) + return + const targetVar = targetNode.vars.find(varItem => varItem.id === varId) + if(!targetVar) + return + targetVar.name = selector[1] + targetVar.selector = selector + }, + ) + return { + nodesWithInspectVars: nodes, + } + }) + }, + deleteInspectVar: (nodeId, varId) => { + set((state: InspectVarsSliceShape) => { + const nodes = produce(state.nodesWithInspectVars, (draft) => { + const targetNode = draft.find(node => node.nodeId === nodeId) + if (!targetNode) + return + const needChangeVarIndex = targetNode.vars.findIndex(varItem => varItem.id === varId) + if (needChangeVarIndex !== -1) + targetNode.vars.splice(needChangeVarIndex, 1) + }, + ) + return { + nodesWithInspectVars: nodes, + } + }) + }, + }) +} diff --git a/web/app/components/workflow/store/workflow/debug/mock-data.ts b/web/app/components/workflow/store/workflow/debug/mock-data.ts new file mode 100644 index 0000000000..9d1bf80076 --- /dev/null +++ b/web/app/components/workflow/store/workflow/debug/mock-data.ts @@ -0,0 +1,72 @@ +import { VarType } from '../../../types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' + +export const vars: VarInInspect[] = [ + { + id: 'xxx', + type: VarInInspectType.node, + name: 'text00', + description: '', + selector: ['1745476079387', 'text'], + value_type: VarType.string, + value: 'text value...', + edited: false, + }, + { + id: 'fdklajljgldjglkagjlk', + type: VarInInspectType.node, + name: 'text', + description: '', + selector: ['1712386917734', 'text'], + value_type: VarType.string, + value: 'made zhizhang', + edited: false, + }, +] + +export const conversationVars: VarInInspect[] = [ + { + id: 'con1', + type: VarInInspectType.conversation, + name: 'conversationVar 1', + description: '', + selector: ['conversation', 'var1'], + value_type: VarType.string, + value: 'conversation var value...', + edited: false, + }, + { + id: 'con2', + type: VarInInspectType.conversation, + name: 'conversationVar 2', + description: '', + selector: ['conversation', 'var2'], + value_type: VarType.number, + value: 456, + edited: false, + }, +] + +export const systemVars: VarInInspect[] = [ + { + id: 'sys1', + type: VarInInspectType.system, + name: 'query', + description: '', + selector: ['sys', 'query'], + value_type: VarType.string, + value: 'Hello robot!', + edited: false, + }, + { + id: 'sys2', + type: VarInInspectType.system, + name: 'user_id', + description: '', + selector: ['sys', 'user_id'], + value_type: VarType.string, + value: 'djflakjerlkjdlksfjslakjsdfl', + edited: false, + }, +] diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts index 0e2f5eb0f7..fe17cc21e0 100644 --- a/web/app/components/workflow/store/workflow/index.ts +++ b/web/app/components/workflow/store/workflow/index.ts @@ -28,7 +28,12 @@ import type { WorkflowDraftSliceShape } from './workflow-draft-slice' import { createWorkflowDraftSlice } from './workflow-draft-slice' import type { WorkflowSliceShape } from './workflow-slice' import { createWorkflowSlice } from './workflow-slice' +import type { InspectVarsSliceShape } from './debug/inspect-vars-slice' +import { createInspectVarsSlice } from './debug/inspect-vars-slice' + import { WorkflowContext } from '@/app/components/workflow/context' +import type { LayoutSliceShape } from './layout-slice' +import { createLayoutSlice } from './layout-slice' import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice' export type Shape = @@ -43,6 +48,8 @@ export type Shape = VersionSliceShape & WorkflowDraftSliceShape & WorkflowSliceShape & + InspectVarsSliceShape & + LayoutSliceShape & WorkflowAppSliceShape type CreateWorkflowStoreParams = { @@ -64,6 +71,8 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { ...createVersionSlice(...args), ...createWorkflowDraftSlice(...args), ...createWorkflowSlice(...args), + ...createInspectVarsSlice(...args), + ...createLayoutSlice(...args), ...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape), })) } diff --git a/web/app/components/workflow/store/workflow/layout-slice.ts b/web/app/components/workflow/store/workflow/layout-slice.ts new file mode 100644 index 0000000000..91c80ba84a --- /dev/null +++ b/web/app/components/workflow/store/workflow/layout-slice.ts @@ -0,0 +1,48 @@ +import type { StateCreator } from 'zustand' + +export type LayoutSliceShape = { + workflowCanvasWidth?: number + workflowCanvasHeight?: number + setWorkflowCanvasWidth: (width: number) => void + setWorkflowCanvasHeight: (height: number) => void + // rightPanelWidth - otherPanelWidth = nodePanelWidth + rightPanelWidth?: number + setRightPanelWidth: (width: number) => void + nodePanelWidth: number + setNodePanelWidth: (width: number) => void + previewPanelWidth: number + setPreviewPanelWidth: (width: number) => void + otherPanelWidth: number + setOtherPanelWidth: (width: number) => void + bottomPanelWidth: number // min-width = 400px; default-width = auto || 480px; + setBottomPanelWidth: (width: number) => void + bottomPanelHeight: number + setBottomPanelHeight: (height: number) => void + variableInspectPanelHeight: number // min-height = 120px; default-height = 320px; + setVariableInspectPanelHeight: (height: number) => void + maximizeCanvas: boolean + setMaximizeCanvas: (maximize: boolean) => void +} + +export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({ + workflowCanvasWidth: undefined, + workflowCanvasHeight: undefined, + setWorkflowCanvasWidth: width => set(() => ({ workflowCanvasWidth: width })), + setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })), + rightPanelWidth: undefined, + setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })), + nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400, + setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })), + previewPanelWidth: localStorage.getItem('debug-and-preview-panel-width') ? Number.parseFloat(localStorage.getItem('debug-and-preview-panel-width')!) : 400, + setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })), + otherPanelWidth: 400, + setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })), + bottomPanelWidth: 480, + setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })), + bottomPanelHeight: 324, + setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })), + variableInspectPanelHeight: localStorage.getItem('workflow-variable-inpsect-panel-height') ? Number.parseFloat(localStorage.getItem('workflow-variable-inpsect-panel-height')!) : 320, + setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })), + maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true', + setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })), +}) diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts index 06a5f45e11..855f45f264 100644 --- a/web/app/components/workflow/store/workflow/panel-slice.ts +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -15,6 +15,10 @@ export type PanelSliceShape = { left: number } setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void + showVariableInspectPanel: boolean + setShowVariableInspectPanel: (showVariableInspectPanel: boolean) => void + initShowLastRunTab: boolean + setInitShowLastRunTab: (initShowLastRunTab: boolean) => void } export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({ @@ -29,4 +33,8 @@ export const createPanelSlice: StateCreator<PanelSliceShape> = set => ({ setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })), panelMenu: undefined, setPanelMenu: panelMenu => set(() => ({ panelMenu })), + showVariableInspectPanel: false, + setShowVariableInspectPanel: showVariableInspectPanel => set(() => ({ showVariableInspectPanel })), + initShowLastRunTab: false, + setInitShowLastRunTab: initShowLastRunTab => set(() => ({ initShowLastRunTab })), }) diff --git a/web/app/components/workflow/store/workflow/tool-slice.ts b/web/app/components/workflow/store/workflow/tool-slice.ts index 2d54bbd925..d6d89abcf0 100644 --- a/web/app/components/workflow/store/workflow/tool-slice.ts +++ b/web/app/components/workflow/store/workflow/tool-slice.ts @@ -10,6 +10,8 @@ export type ToolSliceShape = { setCustomTools: (tools: ToolWithProvider[]) => void workflowTools: ToolWithProvider[] setWorkflowTools: (tools: ToolWithProvider[]) => void + mcpTools: ToolWithProvider[] + setMcpTools: (tools: ToolWithProvider[]) => void toolPublished: boolean setToolPublished: (toolPublished: boolean) => void } @@ -21,6 +23,8 @@ export const createToolSlice: StateCreator<ToolSliceShape> = set => ({ setCustomTools: customTools => set(() => ({ customTools })), workflowTools: [], setWorkflowTools: workflowTools => set(() => ({ workflowTools })), + mcpTools: [], + setMcpTools: mcpTools => set(() => ({ mcpTools })), toolPublished: false, setToolPublished: toolPublished => set(() => ({ toolPublished })), }) diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 5218c23084..507d494c74 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -7,7 +7,7 @@ import type { import type { Resolution, TransferMethod } from '@/types/app' import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' -import type { FileResponse, NodeTracing } from '@/types/workflow' +import type { FileResponse, NodeTracing, PanelProps } from '@/types/workflow' import type { Collection, Tool } from '@/app/components/tools/types' import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import type { @@ -16,6 +16,7 @@ import type { } from '@/app/components/workflow/nodes/_base/components/error-handle/types' import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types' import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types' +import type { PluginMeta } from '../plugins/types' export enum BlockEnum { Start = 'start', @@ -116,6 +117,7 @@ export type NodeProps<T = unknown> = { id: string; data: CommonNodeType<T> } export type NodePanelProps<T> = { id: string data: CommonNodeType<T> + panelProps: PanelProps } export type Edge = ReactFlowEdge<CommonEdgeType> @@ -135,6 +137,7 @@ export type Variable = { variable: string } value_selector: ValueSelector + value_type?: VarType variable_type?: VarKindType value?: string options?: string[] @@ -147,6 +150,7 @@ export type EnvironmentVariable = { name: string value: any value_type: 'string' | 'number' | 'secret' + description: string } export type ConversationVariable = { @@ -198,7 +202,9 @@ export type InputVar = { hint?: string options?: string[] value_selector?: ValueSelector - hide: boolean + getVarValueFromDependent?: boolean + hide?: boolean + isFileItem?: boolean } & Partial<UploadFileSetting> export type ModelConfig = { @@ -259,6 +265,7 @@ export enum VarType { arrayObject = 'array[object]', arrayFile = 'array[file]', any = 'any', + arrayAny = 'array[any]', } export enum ValueType { @@ -297,6 +304,7 @@ export type Block = { export type NodeDefault<T> = { defaultValue: Partial<T> + defaultRunInputData?: Record<string, any> getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[] getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[] checkValid: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean; errorMessage?: string } @@ -325,6 +333,7 @@ export enum NodeRunningStatus { Failed = 'failed', Exception = 'exception', Retry = 'retry', + Stopped = 'stopped', } export type OnNodeAdd = ( @@ -360,7 +369,6 @@ export type WorkflowRunningData = { message_id?: string conversation_id?: string result: { - sequence_number?: number workflow_id?: string inputs?: string process_data?: string @@ -383,9 +391,9 @@ export type WorkflowRunningData = { export type HistoryWorkflowData = { id: string - sequence_number: number status: string conversation_id?: string + finished_at?: number } export enum ChangeType { @@ -403,6 +411,7 @@ export type MoreInfo = { export type ToolWithProvider = Collection & { tools: Tool[] + meta: PluginMeta } export enum SupportUploadFileTypes { diff --git a/web/app/components/workflow/update-dsl-modal.tsx b/web/app/components/workflow/update-dsl-modal.tsx index b789e3b2fd..00c36cce90 100644 --- a/web/app/components/workflow/update-dsl-modal.tsx +++ b/web/app/components/workflow/update-dsl-modal.tsx @@ -86,6 +86,8 @@ const UpdateDSLModal = ({ graph, features, hash, + conversation_variables, + environment_variables, } = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`) const { nodes, edges, viewport } = graph @@ -122,6 +124,8 @@ const UpdateDSLModal = ({ viewport, features: newFeatures, hash, + conversation_variables: conversation_variables || [], + environment_variables: environment_variables || [], }, } as any) }, [eventEmitter]) diff --git a/web/app/components/workflow/utils/common.ts b/web/app/components/workflow/utils/common.ts index 8a8afbb264..8452161950 100644 --- a/web/app/components/workflow/utils/common.ts +++ b/web/app/components/workflow/utils/common.ts @@ -33,3 +33,22 @@ export const isEventTargetInputArea = (target: HTMLElement) => { if (target.contentEditable === 'true') return true } + +/** + * Format workflow run identifier using finished_at timestamp + * @param finishedAt - Unix timestamp in seconds + * @param fallbackText - Text to show when finishedAt is not available (default: 'Running') + * @returns Formatted string like " (14:30:25)" or " (Running)" + */ +export const formatWorkflowRunIdentifier = (finishedAt?: number, fallbackText = 'Running'): string => { + if (!finishedAt) + return ` (${fallbackText})` + + const date = new Date(finishedAt * 1000) + const timeStr = date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + return ` (${timeStr})` +} diff --git a/web/app/components/workflow/utils/layout.ts b/web/app/components/workflow/utils/dagre-layout.ts similarity index 62% rename from web/app/components/workflow/utils/layout.ts rename to web/app/components/workflow/utils/dagre-layout.ts index 3c4189b5bc..5eafe77586 100644 --- a/web/app/components/workflow/utils/layout.ts +++ b/web/app/components/workflow/utils/dagre-layout.ts @@ -19,19 +19,87 @@ import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => { - const dagreGraph = new dagre.graphlib.Graph() + const dagreGraph = new dagre.graphlib.Graph({ compound: true }) dagreGraph.setDefaultEdgeLabel(() => ({})) + const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) + +// The default dagre layout algorithm often fails to correctly order the branches +// of an If/Else node, leading to crossed edges. +// +// To solve this, we employ a "virtual container" strategy: +// 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches. +// 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container. +// 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE). +// +// This forces dagre to treat the ordered branches as an unbreakable, atomic group, +// ensuring their layout respects the intended logical sequence. + const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse) + let virtualLogicApplied = false + + ifElseNodes.forEach((ifElseNode) => { + const childEdges = edges.filter(e => e.source === ifElseNode.id) + if (childEdges.length <= 1) + return + + virtualLogicApplied = true + const sortedChildEdges = childEdges.sort((edgeA, edgeB) => { + const handleA = edgeA.sourceHandle + const handleB = edgeB.sourceHandle + + if (handleA && handleB) { + const cases = (ifElseNode.data as any).cases || [] + const isAElse = handleA === 'false' + const isBElse = handleB === 'false' + + if (isAElse) return 1 + if (isBElse) return -1 + + const indexA = cases.findIndex((c: any) => c.case_id === handleA) + const indexB = cases.findIndex((c: any) => c.case_id === handleB) + + if (indexA !== -1 && indexB !== -1) + return indexA - indexB + } + return 0 + }) + + const parentDummyId = `dummy-parent-${ifElseNode.id}` + dagreGraph.setNode(parentDummyId, { width: 1, height: 1 }) + + const dummyNodes: string[] = [] + sortedChildEdges.forEach((edge) => { + const dummyNodeId = `dummy-${edge.source}-${edge.target}` + dummyNodes.push(dummyNodeId) + dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 }) + dagreGraph.setParent(dummyNodeId, parentDummyId) + + const edgeIndex = edges.findIndex(e => e.id === edge.id) + if (edgeIndex > -1) + edges.splice(edgeIndex, 1) + + edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge) + edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge) + }) + + for (let i = 0; i < dummyNodes.length - 1; i++) { + const sourceDummy = dummyNodes[i] + const targetDummy = dummyNodes[i + 1] + edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge) + } + }) + dagreGraph.setGraph({ rankdir: 'LR', align: 'UL', nodesep: 40, - ranksep: 60, + ranksep: virtualLogicApplied ? 30 : 60, ranker: 'tight-tree', marginx: 30, marginy: 200, }) + nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: node.width!, diff --git a/web/app/components/workflow/utils/debug.ts b/web/app/components/workflow/utils/debug.ts new file mode 100644 index 0000000000..6dd111d714 --- /dev/null +++ b/web/app/components/workflow/utils/debug.ts @@ -0,0 +1,25 @@ +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import { VarType } from '../types' + +type OutputToVarInInspectParams = { + nodeId: string + name: string + value: any +} +export const outputToVarInInspect = ({ + nodeId, + name, + value, +}: OutputToVarInInspectParams): VarInInspect => { + return { + id: `${Date.now()}`, // TODO: wait for api + type: VarInInspectType.node, + name, + description: '', + selector: [nodeId, name], + value_type: VarType.string, // TODO: wait for api or get from node + value, + edited: false, + } +} diff --git a/web/app/components/workflow/utils/index.ts b/web/app/components/workflow/utils/index.ts index 4a1da760d4..0d10551dda 100644 --- a/web/app/components/workflow/utils/index.ts +++ b/web/app/components/workflow/utils/index.ts @@ -1,7 +1,7 @@ export * from './node' export * from './edge' export * from './workflow-init' -export * from './layout' +export * from './dagre-layout' export * from './common' export * from './tool' export * from './workflow' diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index 93a61230ba..dc22d61ca5 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -28,6 +28,7 @@ import type { IfElseNodeType } from '../nodes/if-else/types' import { branchNameCorrect } from '../nodes/if-else/utils' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' +import type { ToolNodeType } from '../nodes/tool/types' import { getIterationStartNode, getLoopStartNode, @@ -276,6 +277,7 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { if (node.data.type === BlockEnum.ParameterExtractor) (node as any).data.model.provider = correctModelProvider((node as any).data.model.provider) + if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) { node.data.retry_config = { retry_enabled: true, @@ -284,6 +286,24 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { } } + if (node.data.type === BlockEnum.Tool && !(node as Node<ToolNodeType>).data.version) { + (node as Node<ToolNodeType>).data.version = '2' + + const toolConfigurations = (node as Node<ToolNodeType>).data.tool_configurations + if (toolConfigurations && Object.keys(toolConfigurations).length > 0) { + const newValues = { ...toolConfigurations } + Object.keys(toolConfigurations).forEach((key) => { + if (typeof toolConfigurations[key] !== 'object' || toolConfigurations[key] === null) { + newValues[key] = { + type: 'constant', + value: toolConfigurations[key], + } + } + }); + (node as Node<ToolNodeType>).data.tool_configurations = newValues + } + } + return node }) } diff --git a/web/app/components/workflow/utils/workflow.ts b/web/app/components/workflow/utils/workflow.ts index 88c31f09b5..93c0d38d4b 100644 --- a/web/app/components/workflow/utils/workflow.ts +++ b/web/app/components/workflow/utils/workflow.ts @@ -19,7 +19,10 @@ import { import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' -export const canRunBySingle = (nodeType: BlockEnum) => { +export const canRunBySingle = (nodeType: BlockEnum, isChildNode: boolean) => { + // child node means in iteration or loop. Set value to iteration(or loop) may cause variable not exit problem in backend. + if(isChildNode && nodeType === BlockEnum.Assigner) + return false return nodeType === BlockEnum.LLM || nodeType === BlockEnum.KnowledgeRetrieval || nodeType === BlockEnum.Code @@ -32,6 +35,10 @@ export const canRunBySingle = (nodeType: BlockEnum) => { || nodeType === BlockEnum.Agent || nodeType === BlockEnum.DocExtractor || nodeType === BlockEnum.Loop + || nodeType === BlockEnum.Start + || nodeType === BlockEnum.IfElse + || nodeType === BlockEnum.VariableAggregator + || nodeType === BlockEnum.Assigner } type ConnectedSourceOrTargetNodesChange = { diff --git a/web/app/components/workflow/variable-inspect/empty.tsx b/web/app/components/workflow/variable-inspect/empty.tsx new file mode 100644 index 0000000000..6b371635d7 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/empty.tsx @@ -0,0 +1,28 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' + +const Empty: FC = () => { + const { t } = useTranslation() + + return ( + <div className='flex h-full flex-col gap-3 rounded-xl bg-background-section p-8'> + <div className='flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur-sm'> + <Variable02 className='h-5 w-5 text-text-accent' /> + </div> + <div className='flex flex-col gap-1'> + <div className='system-sm-semibold text-text-secondary'>{t('workflow.debug.variableInspect.title')}</div> + <div className='system-xs-regular text-text-tertiary'>{t('workflow.debug.variableInspect.emptyTip')}</div> + <a + className='system-xs-regular cursor-pointer text-text-accent' + href='https://docs.dify.ai/en/guides/workflow/debug-and-preview/variable-inspect' + target='_blank' + rel='noopener noreferrer'> + {t('workflow.debug.variableInspect.emptyLink')} + </a> + </div> + </div> + ) +} + +export default Empty diff --git a/web/app/components/workflow/variable-inspect/group.tsx b/web/app/components/workflow/variable-inspect/group.tsx new file mode 100644 index 0000000000..1b032c8992 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/group.tsx @@ -0,0 +1,174 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowRightSLine, + RiDeleteBinLine, + RiFileList3Line, + RiLoader2Line, + // RiErrorWarningFill, +} from '@remixicon/react' +// import Button from '@/app/components/base/button' +import ActionButton from '@/app/components/base/action-button' +import Tooltip from '@/app/components/base/tooltip' +import BlockIcon from '@/app/components/workflow/block-icon' +import { + BubbleX, + Env, +} from '@/app/components/base/icons/src/vender/line/others' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import type { currentVarType } from './panel' +import { VarInInspectType } from '@/types/workflow' +import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import cn from '@/utils/classnames' +import { useToolIcon } from '../hooks' + +type Props = { + nodeData?: NodeWithVar + currentVar?: currentVarType + varType: VarInInspectType + varList: VarInInspect[] + handleSelect: (state: any) => void + handleView?: () => void + handleClear?: () => void +} + +const Group = ({ + nodeData, + currentVar, + varType, + varList, + handleSelect, + handleView, + handleClear, +}: Props) => { + const { t } = useTranslation() + const [isCollapsed, setIsCollapsed] = useState(false) + + const toolIcon = useToolIcon(nodeData?.nodePayload as any) + + const isEnv = varType === VarInInspectType.environment + const isChatVar = varType === VarInInspectType.conversation + const isSystem = varType === VarInInspectType.system + + const visibleVarList = isEnv ? varList : varList.filter(v => v.visible) + + const handleSelectVar = (varItem: any, type?: string) => { + if (type === VarInInspectType.environment) { + handleSelect({ + nodeId: VarInInspectType.environment, + title: VarInInspectType.environment, + nodeType: VarInInspectType.environment, + var: { + ...varItem, + type: VarInInspectType.environment, + ...(varItem.value_type === 'secret' ? { value: '******************' } : {}), + }, + }) + return + } + if (type === VarInInspectType.conversation) { + handleSelect({ + nodeId: VarInInspectType.conversation, + nodeType: VarInInspectType.conversation, + title: VarInInspectType.conversation, + var: { + ...varItem, + type: VarInInspectType.conversation, + }, + }) + return + } + if (type === VarInInspectType.system) { + handleSelect({ + nodeId: VarInInspectType.system, + nodeType: VarInInspectType.system, + title: VarInInspectType.system, + var: { + ...varItem, + type: VarInInspectType.system, + }, + }) + return + } + if (!nodeData) return + handleSelect({ + nodeId: nodeData.nodeId, + nodeType: nodeData.nodeType, + title: nodeData.title, + var: varItem, + }) + } + + return ( + <div className='p-0.5'> + {/* node item */} + <div className='group flex h-6 items-center gap-0.5'> + <div className='h-3 w-3 shrink-0'> + {nodeData?.isSingRunRunning && ( + <RiLoader2Line className='h-3 w-3 animate-spin text-text-accent' /> + )} + {(!nodeData || !nodeData.isSingRunRunning) && visibleVarList.length > 0 && ( + <RiArrowRightSLine className={cn('h-3 w-3 text-text-tertiary', !isCollapsed && 'rotate-90')} onClick={() => setIsCollapsed(!isCollapsed)} /> + )} + </div> + <div className='flex grow cursor-pointer items-center gap-1' onClick={() => setIsCollapsed(!isCollapsed)}> + {nodeData && ( + <> + <BlockIcon + className='shrink-0' + type={nodeData.nodeType} + toolIcon={toolIcon || ''} + size='xs' + /> + <div className='system-xs-medium-uppercase truncate text-text-tertiary'>{nodeData.title}</div> + </> + )} + {!nodeData && ( + <div className='system-xs-medium-uppercase truncate text-text-tertiary'> + {isEnv && t('workflow.debug.variableInspect.envNode')} + {isChatVar && t('workflow.debug.variableInspect.chatNode')} + {isSystem && t('workflow.debug.variableInspect.systemNode')} + </div> + )} + </div> + {nodeData && !nodeData.isSingRunRunning && ( + <div className='hidden shrink-0 items-center group-hover:flex'> + <Tooltip popupContent={t('workflow.debug.variableInspect.view')}> + <ActionButton onClick={handleView}> + <RiFileList3Line className='h-4 w-4' /> + </ActionButton> + </Tooltip> + <Tooltip popupContent={t('workflow.debug.variableInspect.clearNode')}> + <ActionButton onClick={handleClear}> + <RiDeleteBinLine className='h-4 w-4' /> + </ActionButton> + </Tooltip> + </div> + )} + </div> + {/* var item list */} + {!isCollapsed && !nodeData?.isSingRunRunning && ( + <div className='px-0.5'> + {visibleVarList.length > 0 && visibleVarList.map(varItem => ( + <div + key={varItem.id} + className={cn( + 'relative flex cursor-pointer items-center gap-1 rounded-md px-3 py-1 hover:bg-state-base-hover', + varItem.id === currentVar?.var?.id && 'bg-state-base-hover-alt hover:bg-state-base-hover-alt', + )} + onClick={() => handleSelectVar(varItem, varType)} + > + {isEnv && <Env className='h-4 w-4 shrink-0 text-util-colors-violet-violet-600' />} + {isChatVar && <BubbleX className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' />} + {(isSystem || nodeData) && <Variable02 className={cn('h-4 w-4 shrink-0 text-text-accent', ['error_type', 'error_message'].includes(varItem.name) && 'text-text-warning')} />} + <div className='system-sm-medium grow truncate text-text-secondary'>{varItem.name}</div> + <div className='system-xs-regular shrink-0 text-text-tertiary'>{varItem.value_type}</div> + </div> + ))} + </div> + )} + </div> + ) +} + +export default Group diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx new file mode 100644 index 0000000000..2761dcae3c --- /dev/null +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -0,0 +1,61 @@ +import type { FC } from 'react' +import { + useCallback, + useMemo, +} from 'react' +import { debounce } from 'lodash-es' +import { useStore } from '../store' +import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel' +import Panel from './panel' +import cn from '@/utils/classnames' + +const VariableInspectPanel: FC = () => { + const showVariableInspectPanel = useStore(s => s.showVariableInspectPanel) + const workflowCanvasHeight = useStore(s => s.workflowCanvasHeight) + const variableInspectPanelHeight = useStore(s => s.variableInspectPanelHeight) + const setVariableInspectPanelHeight = useStore(s => s.setVariableInspectPanelHeight) + + const maxHeight = useMemo(() => { + if (!workflowCanvasHeight) + return 480 + return workflowCanvasHeight - 60 + }, [workflowCanvasHeight]) + + const handleResize = useCallback((width: number, height: number) => { + localStorage.setItem('workflow-variable-inpsect-panel-height', `${height}`) + setVariableInspectPanelHeight(height) + }, [setVariableInspectPanelHeight]) + + const { + triggerRef, + containerRef, + } = useResizePanel({ + direction: 'vertical', + triggerDirection: 'top', + minHeight: 120, + maxHeight, + onResize: debounce(handleResize), + }) + + if (!showVariableInspectPanel) + return null + + return ( + <div className={cn('relative pb-1')}> + <div + ref={triggerRef} + className='absolute -top-1 left-0 flex h-1 w-full cursor-row-resize resize-y items-center justify-center'> + <div className='h-0.5 w-10 rounded-sm bg-state-base-handle hover:w-full hover:bg-state-accent-solid active:w-full active:bg-state-accent-solid'></div> + </div> + <div + ref={containerRef} + className={cn('overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl')} + style={{ height: `${variableInspectPanelHeight}px` }} + > + <Panel /> + </div> + </div> + ) +} + +export default VariableInspectPanel diff --git a/web/app/components/workflow/variable-inspect/left.tsx b/web/app/components/workflow/variable-inspect/left.tsx new file mode 100644 index 0000000000..ecc5836781 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/left.tsx @@ -0,0 +1,111 @@ +// import { useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { useStore } from '../store' +import Button from '@/app/components/base/button' +// import ActionButton from '@/app/components/base/action-button' +// import Tooltip from '@/app/components/base/tooltip' +import Group from './group' +import useCurrentVars from '../hooks/use-inspect-vars-crud' +import { useNodesInteractions } from '../hooks/use-nodes-interactions' +import type { currentVarType } from './panel' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import cn from '@/utils/classnames' + +type Props = { + currentNodeVar?: currentVarType + handleVarSelect: (state: any) => void +} + +const Left = ({ + currentNodeVar, + handleVarSelect, +}: Props) => { + const { t } = useTranslation() + + const environmentVariables = useStore(s => s.environmentVariables) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + + const { + conversationVars, + systemVars, + nodesWithInspectVars, + deleteAllInspectorVars, + deleteNodeInspectorVars, + } = useCurrentVars() + const { handleNodeSelect } = useNodesInteractions() + + const showDivider = environmentVariables.length > 0 || conversationVars.length > 0 || systemVars.length > 0 + + const handleClearAll = () => { + deleteAllInspectorVars() + setCurrentFocusNodeId('') + } + + const handleClearNode = (nodeId: string) => { + deleteNodeInspectorVars(nodeId) + setCurrentFocusNodeId('') + } + + return ( + <div className={cn('flex h-full flex-col')}> + {/* header */} + <div className='flex shrink-0 items-center justify-between gap-1 pl-4 pr-1 pt-2'> + <div className='system-sm-semibold-uppercase truncate text-text-primary'>{t('workflow.debug.variableInspect.title')}</div> + <Button variant='ghost' size='small' className='shrink-0' onClick={handleClearAll}>{t('workflow.debug.variableInspect.clearAll')}</Button> + </div> + {/* content */} + <div className='grow overflow-y-auto py-1'> + {/* group ENV */} + {environmentVariables.length > 0 && ( + <Group + varType={VarInInspectType.environment} + varList={environmentVariables as VarInInspect[]} + currentVar={currentNodeVar} + handleSelect={handleVarSelect} + /> + )} + {/* group CHAT VAR */} + {conversationVars.length > 0 && ( + <Group + varType={VarInInspectType.conversation} + varList={conversationVars} + currentVar={currentNodeVar} + handleSelect={handleVarSelect} + /> + )} + {/* group SYSTEM VAR */} + {systemVars.length > 0 && ( + <Group + varType={VarInInspectType.system} + varList={systemVars} + currentVar={currentNodeVar} + handleSelect={handleVarSelect} + /> + )} + {/* divider */} + {showDivider && ( + <div className='px-4 py-1'> + <div className='h-px bg-divider-subtle'></div> + </div> + )} + {/* group nodes */} + {nodesWithInspectVars.length > 0 && nodesWithInspectVars.map(group => ( + <Group + key={group.nodeId} + varType={VarInInspectType.node} + varList={group.vars} + nodeData={group} + currentVar={currentNodeVar} + handleSelect={handleVarSelect} + handleView={() => handleNodeSelect(group.nodeId, false, true)} + handleClear={() => handleClearNode(group.nodeId)} + /> + ))} + </div> + </div> + ) +} + +export default Left diff --git a/web/app/components/workflow/variable-inspect/panel.tsx b/web/app/components/workflow/variable-inspect/panel.tsx new file mode 100644 index 0000000000..be55142e29 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/panel.tsx @@ -0,0 +1,188 @@ +import type { FC } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, +} from '@remixicon/react' +import { useStore } from '../store' +import useCurrentVars from '../hooks/use-inspect-vars-crud' +import Empty from './empty' +import Left from './left' +import Right from './right' +import ActionButton from '@/app/components/base/action-button' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' + +import cn from '@/utils/classnames' + +export type currentVarType = { + nodeId: string + nodeType: string + title: string + isValueFetched?: boolean + var: VarInInspect +} + +const Panel: FC = () => { + const { t } = useTranslation() + + const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) + const [showLeftPanel, setShowLeftPanel] = useState(true) + + const environmentVariables = useStore(s => s.environmentVariables) + const currentFocusNodeId = useStore(s => s.currentFocusNodeId) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + const [currentVarId, setCurrentVarId] = useState('') + + const { + conversationVars, + systemVars, + nodesWithInspectVars, + fetchInspectVarValue, + } = useCurrentVars() + + const isEmpty = useMemo(() => { + const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars] + return allVars.length === 0 + }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + + const currentNodeInfo = useMemo(() => { + if (!currentFocusNodeId) return + if (currentFocusNodeId === VarInInspectType.environment) { + const currentVar = environmentVariables.find(v => v.id === currentVarId) + const res = { + nodeId: VarInInspectType.environment, + title: VarInInspectType.environment, + nodeType: VarInInspectType.environment, + } + if (currentVar) { + return { + ...res, + var: { + ...currentVar, + type: VarInInspectType.environment, + visible: true, + ...(currentVar.value_type === 'secret' ? { value: '******************' } : {}), + }, + } + } + return res + } + if (currentFocusNodeId === VarInInspectType.conversation) { + const currentVar = conversationVars.find(v => v.id === currentVarId) + const res = { + nodeId: VarInInspectType.conversation, + title: VarInInspectType.conversation, + nodeType: VarInInspectType.conversation, + } + if (currentVar) { + return { + ...res, + var: { + ...currentVar, + type: VarInInspectType.conversation, + }, + } + } + return res + } + if (currentFocusNodeId === VarInInspectType.system) { + const currentVar = systemVars.find(v => v.id === currentVarId) + const res = { + nodeId: VarInInspectType.system, + title: VarInInspectType.system, + nodeType: VarInInspectType.system, + } + if (currentVar) { + return { + ...res, + var: { + ...currentVar, + type: VarInInspectType.system, + }, + } + } + return res + } + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + if (!targetNode) return + const currentVar = targetNode.vars.find(v => v.id === currentVarId) + return { + nodeId: targetNode.nodeId, + nodeType: targetNode.nodeType, + title: targetNode.title, + isSingRunRunning: targetNode.isSingRunRunning, + isValueFetched: targetNode.isValueFetched, + ...(currentVar ? { var: currentVar } : {}), + } + }, [currentFocusNodeId, currentVarId, environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + + const isCurrentNodeVarValueFetching = useMemo(() => { + if (!currentNodeInfo) return false + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentNodeInfo.nodeId) + if (!targetNode) return false + return !targetNode.isValueFetched + }, [currentNodeInfo, nodesWithInspectVars]) + + const handleNodeVarSelect = useCallback((node: currentVarType) => { + setCurrentFocusNodeId(node.nodeId) + setCurrentVarId(node.var.id) + }, [setCurrentFocusNodeId, setCurrentVarId]) + + useEffect(() => { + if (currentFocusNodeId && currentVarId) { + const targetNode = nodesWithInspectVars.find(node => node.nodeId === currentFocusNodeId) + if (targetNode && !targetNode.isValueFetched) + fetchInspectVarValue([currentFocusNodeId]) + } + }, [currentFocusNodeId, currentVarId, nodesWithInspectVars, fetchInspectVarValue]) + + if (isEmpty) { + return ( + <div className={cn('flex h-full flex-col')}> + <div className='flex shrink-0 items-center justify-between pl-4 pr-2 pt-2'> + <div className='system-sm-semibold-uppercase text-text-primary'>{t('workflow.debug.variableInspect.title')}</div> + <ActionButton onClick={() => setShowVariableInspectPanel(false)}> + <RiCloseLine className='h-4 w-4' /> + </ActionButton> + </div> + <div className='grow p-2'> + <Empty /> + </div> + </div> + ) + } + + return ( + <div className={cn('relative flex h-full')}> + {/* left */} + {bottomPanelWidth < 488 && showLeftPanel && <div className='absolute left-0 top-0 h-full w-full' onClick={() => setShowLeftPanel(false)}></div>} + <div + className={cn( + 'w-60 shrink-0 border-r border-divider-burn', + bottomPanelWidth < 488 + ? showLeftPanel + ? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm' + : 'hidden' + : 'block', + )} + > + <Left + currentNodeVar={currentNodeInfo as currentVarType} + handleVarSelect={handleNodeVarSelect} + /> + </div> + {/* right */} + <div className='w-0 grow'> + <Right + isValueFetching={isCurrentNodeVarValueFetching} + currentNodeVar={currentNodeInfo as currentVarType} + handleOpenMenu={() => setShowLeftPanel(true)} + /> + </div> + </div> + ) +} + +export default Panel diff --git a/web/app/components/workflow/variable-inspect/right.tsx b/web/app/components/workflow/variable-inspect/right.tsx new file mode 100644 index 0000000000..6ddd0d47d3 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/right.tsx @@ -0,0 +1,161 @@ +import { useTranslation } from 'react-i18next' +import { + RiArrowGoBackLine, + RiCloseLine, + RiMenuLine, +} from '@remixicon/react' +import { useStore } from '../store' +import type { BlockEnum } from '../types' +import useCurrentVars from '../hooks/use-inspect-vars-crud' +import Empty from './empty' +import ValueContent from './value-content' +import ActionButton from '@/app/components/base/action-button' +import Badge from '@/app/components/base/badge' +import CopyFeedback from '@/app/components/base/copy-feedback' +import Tooltip from '@/app/components/base/tooltip' +import BlockIcon from '@/app/components/workflow/block-icon' +import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import Loading from '@/app/components/base/loading' +import type { currentVarType } from './panel' +import { VarInInspectType } from '@/types/workflow' +import cn from '@/utils/classnames' + +type Props = { + currentNodeVar?: currentVarType + handleOpenMenu: () => void + isValueFetching?: boolean +} + +const Right = ({ + currentNodeVar, + handleOpenMenu, + isValueFetching, +}: Props) => { + const { t } = useTranslation() + const bottomPanelWidth = useStore(s => s.bottomPanelWidth) + const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + + const { + resetConversationVar, + resetToLastRunVar, + editInspectVarValue, + } = useCurrentVars() + + const handleValueChange = (varId: string, value: any) => { + if (!currentNodeVar) return + editInspectVarValue(currentNodeVar.nodeId, varId, value) + } + + const resetValue = () => { + if (!currentNodeVar) return + resetToLastRunVar(currentNodeVar.nodeId, currentNodeVar.var.id) + } + + const handleClose = () => { + setShowVariableInspectPanel(false) + setCurrentFocusNodeId('') + } + + const handleClear = () => { + if (!currentNodeVar) return + resetConversationVar(currentNodeVar.var.id) + } + + const getCopyContent = () => { + const value = currentNodeVar?.var.value + if (value === null || value === undefined) + return '' + + if (typeof value === 'object') + return JSON.stringify(value) + + return String(value) + } + + return ( + <div className={cn('flex h-full flex-col')}> + {/* header */} + <div className='flex shrink-0 items-center justify-between gap-1 px-2 pt-2'> + {bottomPanelWidth < 488 && ( + <ActionButton className='shrink-0' onClick={handleOpenMenu}> + <RiMenuLine className='h-4 w-4' /> + </ActionButton> + )} + <div className='flex w-0 grow items-center gap-1'> + {currentNodeVar && ( + <> + {currentNodeVar.nodeType === VarInInspectType.environment && ( + <Env className='h-4 w-4 shrink-0 text-util-colors-violet-violet-600' /> + )} + {currentNodeVar.nodeType === VarInInspectType.conversation && ( + <BubbleX className='h-4 w-4 shrink-0 text-util-colors-teal-teal-700' /> + )} + {currentNodeVar.nodeType === VarInInspectType.system && ( + <Variable02 className='h-4 w-4 shrink-0 text-text-accent' /> + )} + {currentNodeVar.nodeType !== VarInInspectType.environment && currentNodeVar.nodeType !== VarInInspectType.conversation && currentNodeVar.nodeType !== VarInInspectType.system && ( + <> + <BlockIcon + className='shrink-0' + type={currentNodeVar.nodeType as BlockEnum} + size='xs' + /> + <div className='system-sm-regular shrink-0 text-text-secondary'>{currentNodeVar.title}</div> + <div className='system-sm-regular shrink-0 text-text-quaternary'>/</div> + </> + )} + <div title={currentNodeVar.var.name} className='system-sm-semibold truncate text-text-secondary'>{currentNodeVar.var.name}</div> + <div className='system-xs-medium ml-1 shrink-0 text-text-tertiary'>{currentNodeVar.var.value_type}</div> + </> + )} + </div> + <div className='flex shrink-0 items-center gap-1'> + {currentNodeVar && ( + <> + {currentNodeVar.var.edited && ( + <Badge> + <span className='ml-[2.5px] mr-[4.5px] h-[3px] w-[3px] rounded bg-text-accent-secondary'></span> + <span className='system-2xs-semibold-uupercase'>{t('workflow.debug.variableInspect.edited')}</span> + </Badge> + )} + {currentNodeVar.var.edited && currentNodeVar.var.type !== VarInInspectType.conversation && ( + <Tooltip popupContent={t('workflow.debug.variableInspect.reset')}> + <ActionButton onClick={resetValue}> + <RiArrowGoBackLine className='h-4 w-4' /> + </ActionButton> + </Tooltip> + )} + {currentNodeVar.var.edited && currentNodeVar.var.type === VarInInspectType.conversation && ( + <Tooltip popupContent={t('workflow.debug.variableInspect.resetConversationVar')}> + <ActionButton onClick={handleClear}> + <RiArrowGoBackLine className='h-4 w-4' /> + </ActionButton> + </Tooltip> + )} + {currentNodeVar.var.value_type !== 'secret' && ( + <CopyFeedback content={getCopyContent()} /> + )} + </> + )} + <ActionButton onClick={handleClose}> + <RiCloseLine className='h-4 w-4' /> + </ActionButton> + </div> + </div> + {/* content */} + <div className='grow p-2'> + {!currentNodeVar && <Empty />} + {isValueFetching && ( + <div className='flex h-full items-center justify-center'> + <Loading /> + </div> + )} + {currentNodeVar && !isValueFetching && <ValueContent currentVar={currentNodeVar.var} handleValueChange={handleValueChange} />} + </div> + </div> + ) +} + +export default Right diff --git a/web/app/components/workflow/variable-inspect/trigger.tsx b/web/app/components/workflow/variable-inspect/trigger.tsx new file mode 100644 index 0000000000..a45c589ab9 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/trigger.tsx @@ -0,0 +1,131 @@ +import type { FC } from 'react' +import { useMemo } from 'react' +import { useNodes } from 'reactflow' +import { useTranslation } from 'react-i18next' +import { RiLoader2Line, RiStopCircleFill } from '@remixicon/react' +import Tooltip from '@/app/components/base/tooltip' +import { useStore } from '../store' +import useCurrentVars from '../hooks/use-inspect-vars-crud' +import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import type { CommonNodeType } from '@/app/components/workflow/types' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' +import cn from '@/utils/classnames' +import { useNodesReadOnly } from '../hooks/use-workflow' + +const VariableInspectTrigger: FC = () => { + const { t } = useTranslation() + const { eventEmitter } = useEventEmitterContextContext() + + const showVariableInspectPanel = useStore(s => s.showVariableInspectPanel) + const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel) + + const environmentVariables = useStore(s => s.environmentVariables) + const setCurrentFocusNodeId = useStore(s => s.setCurrentFocusNodeId) + const { + conversationVars, + systemVars, + nodesWithInspectVars, + deleteAllInspectorVars, + } = useCurrentVars() + const currentVars = useMemo(() => { + const allVars = [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars] + return allVars + }, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars]) + const { + nodesReadOnly, + getNodesReadOnly, + } = useNodesReadOnly() + const workflowRunningData = useStore(s => s.workflowRunningData) + const nodes = useNodes<CommonNodeType>() + const isStepRunning = useMemo(() => nodes.some(node => node.data._singleRunningStatus === NodeRunningStatus.Running), [nodes]) + const isPreviewRunning = useMemo(() => { + if (!workflowRunningData) + return false + return workflowRunningData.result.status === WorkflowRunningStatus.Running + }, [workflowRunningData]) + const isRunning = useMemo(() => isPreviewRunning || isStepRunning, [isPreviewRunning, isStepRunning]) + + const handleStop = () => { + eventEmitter?.emit({ + type: EVENT_WORKFLOW_STOP, + } as any) + } + + const handleClearAll = () => { + deleteAllInspectorVars() + setCurrentFocusNodeId('') + } + + if (showVariableInspectPanel) + return null + + return ( + <div className={cn('flex items-center gap-1')}> + {!isRunning && !currentVars.length && ( + <div + className={cn('system-2xs-semibold-uppercase flex h-5 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-background-default-hover', + nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled', + )} + onClick={() => { + if (getNodesReadOnly()) + return + setShowVariableInspectPanel(true) + }} + > + {t('workflow.debug.variableInspect.trigger.normal')} + </div> + )} + {!isRunning && currentVars.length > 0 && ( + <> + <div + className={cn('system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent', + nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled', + )} + onClick={() => { + if (getNodesReadOnly()) + return + setShowVariableInspectPanel(true) + }} + > + {t('workflow.debug.variableInspect.trigger.cached')} + </div> + <div + className={cn('system-xs-medium flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent hover:text-text-accent', + nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled', + )} + onClick={handleClearAll} + > + {t('workflow.debug.variableInspect.trigger.clear')} + </div> + </> + )} + {isRunning && ( + <> + <div + className='system-xs-medium flex h-6 cursor-pointer items-center gap-1 rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-2 text-text-accent shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent' + onClick={() => setShowVariableInspectPanel(true)} + > + <RiLoader2Line className='h-4 w-4' /> + <span className='text-text-accent'>{t('workflow.debug.variableInspect.trigger.running')}</span> + </div> + {isPreviewRunning && ( + <Tooltip + popupContent={t('workflow.debug.variableInspect.trigger.stop')} + > + <div + className='flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent' + onClick={handleStop} + > + <RiStopCircleFill className='h-4 w-4 text-text-accent' /> + </div> + </Tooltip> + )} + </> + )} + </div> + ) +} + +export default VariableInspectTrigger diff --git a/web/app/components/workflow/variable-inspect/types.ts b/web/app/components/workflow/variable-inspect/types.ts new file mode 100644 index 0000000000..f2e205d4b1 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/types.ts @@ -0,0 +1 @@ +export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP' diff --git a/web/app/components/workflow/variable-inspect/utils.tsx b/web/app/components/workflow/variable-inspect/utils.tsx new file mode 100644 index 0000000000..482ed46c68 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/utils.tsx @@ -0,0 +1,33 @@ +import { z } from 'zod' + +const arrayStringSchemaParttern = z.array(z.string()) +const arrayNumberSchemaParttern = z.array(z.number()) + +// # jsonSchema from https://zod.dev/?id=json-type +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) +type Literal = z.infer<typeof literalSchema> +type Json = Literal | { [key: string]: Json } | Json[] +const jsonSchema: z.ZodType<Json> = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) +const arrayJsonSchema: z.ZodType<Json[]> = z.lazy(() => z.array(jsonSchema)) + +export const validateJSONSchema = (schema: any, type: string) => { + if (type === 'array[string]') { + const result = arrayStringSchemaParttern.safeParse(schema) + return result + } + else if (type === 'array[number]') { + const result = arrayNumberSchemaParttern.safeParse(schema) + return result + } + else if (type === 'object') { + const result = jsonSchema.safeParse(schema) + return result + } + else if (type === 'array[object]') { + const result = arrayJsonSchema.safeParse(schema) + return result + } + else { + return { success: true } as any + } +} diff --git a/web/app/components/workflow/variable-inspect/value-content.tsx b/web/app/components/workflow/variable-inspect/value-content.tsx new file mode 100644 index 0000000000..3770dbb022 --- /dev/null +++ b/web/app/components/workflow/variable-inspect/value-content.tsx @@ -0,0 +1,225 @@ +import { useEffect, useRef, useState } from 'react' +import { useDebounceFn } from 'ahooks' +import Textarea from '@/app/components/base/textarea' +import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor' +import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' +import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message' +import { + checkJsonSchemaDepth, + getValidationErrorMessage, + validateSchemaAgainstDraft7, +} from '@/app/components/workflow/nodes/llm/utils' +import { + validateJSONSchema, +} from '@/app/components/workflow/variable-inspect/utils' +import { useFeatures } from '@/app/components/base/features/hooks' +import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' +import { JSON_SCHEMA_MAX_DEPTH } from '@/config' +import { TransferMethod } from '@/types/app' +import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import cn from '@/utils/classnames' + +type Props = { + currentVar: VarInInspect + handleValueChange: (varId: string, value: any) => void +} + +const ValueContent = ({ + currentVar, + handleValueChange, +}: Props) => { + const contentContainerRef = useRef<HTMLDivElement>(null) + const errorMessageRef = useRef<HTMLDivElement>(null) + const [editorHeight, setEditorHeight] = useState(0) + const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number' + const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files' + const showJSONEditor = !isSysFiles && (currentVar.value_type === 'object' || currentVar.value_type === 'array[string]' || currentVar.value_type === 'array[number]' || currentVar.value_type === 'array[object]' || currentVar.value_type === 'array[any]') + const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]' + const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files') + const JSONEditorDisabled = currentVar.value_type === 'array[any]' + + const formatFileValue = (value: VarInInspect) => { + if (value.value_type === 'file') + return value.value ? getProcessedFilesFromResponse([value.value]) : [] + if (value.value_type === 'array[file]' || (value.type === VarInInspectType.system && currentVar.name === 'files')) + return value.value && value.value.length > 0 ? getProcessedFilesFromResponse(value.value) : [] + return [] + } + + const [value, setValue] = useState<any>() + const [json, setJson] = useState('') + const [parseError, setParseError] = useState<Error | null>(null) + const [validationError, setValidationError] = useState<string>('') + const fileFeature = useFeatures(s => s.features.file) + const [fileValue, setFileValue] = useState<any>(formatFileValue(currentVar)) + + const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 }) + + // update default value when id changed + useEffect(() => { + if (showTextEditor) { + if (currentVar.value_type === 'number') + return setValue(JSON.stringify(currentVar.value)) + if (!currentVar.value) + return setValue('') + setValue(currentVar.value) + } + if (showJSONEditor) + setJson(currentVar.value ? JSON.stringify(currentVar.value, null, 2) : '') + + if (showFileEditor) + setFileValue(formatFileValue(currentVar)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentVar.id, currentVar.value]) + + const handleTextChange = (value: string) => { + if (currentVar.value_type === 'string') + setValue(value) + + if (currentVar.value_type === 'number') { + if (/^-?\d+(\.)?(\d+)?$/.test(value)) + setValue(Number.parseFloat(value)) + } + const newValue = currentVar.value_type === 'number' ? Number.parseFloat(value) : value + debounceValueChange(currentVar.id, newValue) + } + + const jsonValueValidate = (value: string, type: string) => { + try { + const newJSONSchema = JSON.parse(value) + setParseError(null) + const result = validateJSONSchema(newJSONSchema, type) + if (!result.success) { + setValidationError(result.error.message) + return false + } + if (type === 'object' || type === 'array[object]') { + const schemaDepth = checkJsonSchemaDepth(newJSONSchema) + if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) { + setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`) + return false + } + const validationErrors = validateSchemaAgainstDraft7(newJSONSchema) + if (validationErrors.length > 0) { + setValidationError(getValidationErrorMessage(validationErrors)) + return false + } + } + setValidationError('') + return true + } + catch (error) { + setValidationError('') + if (error instanceof Error) { + setParseError(error) + return false + } + else { + setParseError(new Error('Invalid JSON')) + return false + } + } + } + + const handleEditorChange = (value: string) => { + setJson(value) + if (jsonValueValidate(value, currentVar.value_type)) { + const parsed = JSON.parse(value) + debounceValueChange(currentVar.id, parsed) + } + } + + const fileValueValidate = (fileList: any[]) => fileList.every(file => file.upload_file_id) + + const handleFileChange = (value: any[]) => { + setFileValue(value) + // check every file upload progress + // invoke update api after every file uploaded + if (!fileValueValidate(value)) + return + if (currentVar.value_type === 'file') + debounceValueChange(currentVar.id, value[0]) + if (currentVar.value_type === 'array[file]' || isSysFiles) + debounceValueChange(currentVar.id, value) + } + + // get editor height + useEffect(() => { + if (contentContainerRef.current && errorMessageRef.current) { + const errorMessageObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize } = entry.borderBoxSize[0] + const height = (contentContainerRef.current as any).clientHeight - inlineSize + setEditorHeight(height) + } + }) + errorMessageObserver.observe(errorMessageRef.current) + return () => { + errorMessageObserver.disconnect() + } + } + }, [setEditorHeight]) + + return ( + <div + ref={contentContainerRef} + className='flex h-full flex-col' + > + <div className={cn('grow')} style={{ height: `${editorHeight}px` }}> + {showTextEditor && ( + <Textarea + readOnly={textEditorDisabled} + disabled={textEditorDisabled} + className='h-full' + value={value as any} + onChange={e => handleTextChange(e.target.value)} + /> + )} + {showJSONEditor && ( + <SchemaEditor + readonly={JSONEditorDisabled} + className='overflow-y-auto' + hideTopMenu + schema={json} + onUpdate={handleEditorChange} + /> + )} + {showFileEditor && ( + <div className='max-w-[460px]'> + <FileUploaderInAttachmentWrapper + value={fileValue} + onChange={files => handleFileChange(getProcessedFiles(files))} + fileConfig={{ + allowed_file_types: [ + SupportUploadFileTypes.image, + SupportUploadFileTypes.document, + SupportUploadFileTypes.audio, + SupportUploadFileTypes.video, + ], + allowed_file_extensions: [ + ...FILE_EXTS[SupportUploadFileTypes.image], + ...FILE_EXTS[SupportUploadFileTypes.document], + ...FILE_EXTS[SupportUploadFileTypes.audio], + ...FILE_EXTS[SupportUploadFileTypes.video], + ], + allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url], + number_limits: currentVar.value_type === 'file' ? 1 : (fileFeature as any).fileUploadConfig?.workflow_file_upload_limit || 5, + fileUploadConfig: (fileFeature as any).fileUploadConfig, + }} + isDisabled={textEditorDisabled} + /> + </div> + )} + </div> + <div ref={errorMessageRef} className='shrink-0'> + {parseError && <ErrorMessage className='mt-1' message={parseError.message} />} + {validationError && <ErrorMessage className='mt-1' message={validationError} />} + </div> + </div> + ) +} + +export default ValueContent diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index 980e36eb93..3925695895 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -1,7 +1,6 @@ 'use client' import { - useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -23,13 +22,11 @@ import { import { useProviderContext } from '@/context/provider-context' import { useToastContext } from '@/app/components/base/toast' import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants' -import { getLocaleOnClient } from '@/i18n' import { noop } from 'lodash-es' import DifyLogo from '../components/base/logo/dify-logo' - +import { useDocLink } from '@/context/i18n' const EducationApplyAge = () => { const { t } = useTranslation() - const locale = getLocaleOnClient() const [schoolName, setSchoolName] = useState('') const [role, setRole] = useState('Student') const [ageChecked, setAgeChecked] = useState(false) @@ -43,14 +40,7 @@ const EducationApplyAge = () => { const updateEducationStatus = useInvalidateEducationStatus() const { notify } = useToastContext() const router = useRouter() - - const docLink = useMemo(() => { - if (locale === 'zh-Hans') - return 'https://docs.dify.ai/zh-hans/getting-started/dify-for-education' - if (locale === 'ja-JP') - return 'https://docs.dify.ai/ja-jp/getting-started/dify-for-education' - return 'https://docs.dify.ai/getting-started/dify-for-education' - }, [locale]) + const docLink = useDocLink() const handleModalConfirm = () => { setShowModal(undefined) @@ -167,7 +157,7 @@ const EducationApplyAge = () => { <div className='mb-4 mt-5 h-[1px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]'></div> <a className='system-xs-regular flex items-center text-text-accent' - href={docLink} + href={docLink('/getting-started/dify-for-education')} target='_blank' > {t('education.learn')} diff --git a/web/app/education-apply/verify-state-modal.tsx b/web/app/education-apply/verify-state-modal.tsx index aace6a3bb1..2ea2fe5bae 100644 --- a/web/app/education-apply/verify-state-modal.tsx +++ b/web/app/education-apply/verify-state-modal.tsx @@ -1,11 +1,11 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { RiExternalLinkLine, } from '@remixicon/react' import Button from '@/app/components/base/button' -import { getLocaleOnClient } from '@/i18n' +import { useDocLink } from '@/context/i18n' export type IConfirm = { className?: string @@ -30,20 +30,13 @@ function Confirm({ email, }: IConfirm) { const { t } = useTranslation() - const locale = getLocaleOnClient() + const docLink = useDocLink() const dialogRef = useRef<HTMLDivElement>(null) const [isVisible, setIsVisible] = useState(isShow) - - const docLink = useMemo(() => { - if (locale === 'zh-Hans') - return 'https://docs.dify.ai/zh-hans/getting-started/dify-for-education' - if (locale === 'ja-JP') - return 'https://docs.dify.ai/ja-jp/getting-started/dify-for-education' - return 'https://docs.dify.ai/getting-started/dify-for-education' - }, [locale]) + const eduDocLink = docLink('/getting-started/dify-for-education') const handleClick = () => { - window.open(docLink, '_blank', 'noopener,noreferrer') + window.open(eduDocLink, '_blank', 'noopener,noreferrer') } useEffect(() => { @@ -106,7 +99,7 @@ function Confirm({ <div className='flex items-center gap-1'> {showLink && ( <> - <a onClick={handleClick} href={docLink} target='_blank' className='system-xs-regular cursor-pointer text-text-accent'>{t('education.learn')}</a> + <a onClick={handleClick} href={eduDocLink} target='_blank' className='system-xs-regular cursor-pointer text-text-accent'>{t('education.learn')}</a> <RiExternalLinkLine className='h-3 w-3 text-text-accent' /> </> )} diff --git a/web/app/forgot-password/ChangePasswordForm.tsx b/web/app/forgot-password/ChangePasswordForm.tsx index 1ab56102f7..cb02ca44db 100644 --- a/web/app/forgot-password/ChangePasswordForm.tsx +++ b/web/app/forgot-password/ChangePasswordForm.tsx @@ -11,8 +11,7 @@ import Button from '@/app/components/base/button' import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common' import Toast from '@/app/components/base/toast' import Loading from '@/app/components/base/loading' - -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ +import { validPassword } from '@/config' const ChangePasswordForm = () => { const { t } = useTranslation() diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index e549eba521..b62f1213a0 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -17,8 +17,8 @@ import Button from '@/app/components/base/button' import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common' import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' import useDocumentTitle from '@/hooks/use-document-title' - -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ +import { useDocLink } from '@/context/i18n' +import { validPassword } from '@/config' const accountFormSchema = z.object({ email: z @@ -36,6 +36,7 @@ type AccountFormValues = z.infer<typeof accountFormSchema> const InstallForm = () => { useDocumentTitle('') const { t } = useTranslation() + const docLink = useDocLink() const router = useRouter() const [showPassword, setShowPassword] = React.useState(false) const [loading, setLoading] = React.useState(true) @@ -174,7 +175,7 @@ const InstallForm = () => { <Link className='text-text-accent' target='_blank' rel='noopener noreferrer' - href={'https://docs.dify.ai/user-agreement/open-source'} + href={docLink('/policies/open-source')} >{t('login.license.link')}</Link> </div> </div> diff --git a/web/app/layout.tsx b/web/app/layout.tsx index e39b4f3a1b..525445db30 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -9,6 +9,7 @@ import { ThemeProvider } from 'next-themes' import './styles/globals.css' import './styles/markdown.scss' import GlobalPublicStoreProvider from '@/context/global-public-context' +import { DatasetAttr } from '@/types/feature' export const viewport: Viewport = { width: 'device-width', @@ -25,6 +26,30 @@ const LocaleLayout = async ({ }) => { const locale = await getLocaleOnServer() + const datasetMap: Record<DatasetAttr, string | undefined> = { + [DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX, + [DatasetAttr.DATA_PUBLIC_API_PREFIX]: process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX, + [DatasetAttr.DATA_MARKETPLACE_API_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX, + [DatasetAttr.DATA_MARKETPLACE_URL_PREFIX]: process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX, + [DatasetAttr.DATA_PUBLIC_EDITION]: process.env.NEXT_PUBLIC_EDITION, + [DatasetAttr.DATA_PUBLIC_SUPPORT_MAIL_LOGIN]: process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN, + [DatasetAttr.DATA_PUBLIC_SENTRY_DSN]: process.env.NEXT_PUBLIC_SENTRY_DSN, + [DatasetAttr.DATA_PUBLIC_MAINTENANCE_NOTICE]: process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE, + [DatasetAttr.DATA_PUBLIC_SITE_ABOUT]: process.env.NEXT_PUBLIC_SITE_ABOUT, + [DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS]: process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, + [DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM]: process.env.NEXT_PUBLIC_MAX_TOOLS_NUM, + [DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT]: process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT, + [DatasetAttr.DATA_PUBLIC_TOP_K_MAX_VALUE]: process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE, + [DatasetAttr.DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH]: process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH, + [DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT]: process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT, + [DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM]: process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, + [DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH]: process.env.NEXT_PUBLIC_MAX_TREE_DEPTH, + [DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME]: process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, + [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER, + [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, + [DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL]: process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, + } + return ( <html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning> <head> @@ -35,25 +60,7 @@ const LocaleLayout = async ({ </head> <body className="color-scheme h-full select-auto" - data-api-prefix={process.env.NEXT_PUBLIC_API_PREFIX} - data-pubic-api-prefix={process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX} - data-marketplace-api-prefix={process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX} - data-marketplace-url-prefix={process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX} - data-public-edition={process.env.NEXT_PUBLIC_EDITION} - data-public-support-mail-login={process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN} - data-public-sentry-dsn={process.env.NEXT_PUBLIC_SENTRY_DSN} - data-public-maintenance-notice={process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE} - data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT} - data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS} - data-public-max-tools-num={process.env.NEXT_PUBLIC_MAX_TOOLS_NUM} - data-public-max-parallel-limit={process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT} - data-public-top-k-max-value={process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE} - data-public-indexing-max-segmentation-tokens-length={process.env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH} - data-public-loop-node-max-count={process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT} - data-public-max-iterations-num={process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM} - data-public-enable-website-jinareader={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER} - data-public-enable-website-firecrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL} - data-public-enable-website-watercrawl={process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL} + {...datasetMap} > <BrowserInitor> <SentryInitor> diff --git a/web/app/oauth-callback/page.tsx b/web/app/oauth-callback/page.tsx new file mode 100644 index 0000000000..38355f435e --- /dev/null +++ b/web/app/oauth-callback/page.tsx @@ -0,0 +1,10 @@ +'use client' +import { useOAuthCallback } from '@/hooks/use-oauth' + +const OAuthCallback = () => { + useOAuthCallback() + + return <div /> +} + +export default OAuthCallback diff --git a/web/app/reset-password/set-password/page.tsx b/web/app/reset-password/set-password/page.tsx index ee4c114a77..18b7ac12fb 100644 --- a/web/app/reset-password/set-password/page.tsx +++ b/web/app/reset-password/set-password/page.tsx @@ -9,8 +9,7 @@ import Button from '@/app/components/base/button' import { changePasswordWithToken } from '@/service/common' import Toast from '@/app/components/base/toast' import Input from '@/app/components/base/input' - -const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ +import { validPassword } from '@/config' const ChangePasswordForm = () => { const { t } = useTranslation() diff --git a/web/app/signin/LoginLogo.tsx b/web/app/signin/LoginLogo.tsx deleted file mode 100644 index 73dfb88205..0000000000 --- a/web/app/signin/LoginLogo.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client' -import type { FC } from 'react' -import classNames from '@/utils/classnames' -import { useGlobalPublicStore } from '@/context/global-public-context' -import { useTheme } from 'next-themes' - -type LoginLogoProps = { - className?: string -} - -const LoginLogo: FC<LoginLogoProps> = ({ - className, -}) => { - const { systemFeatures } = useGlobalPublicStore() - const { theme } = useTheme() - - let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` - if (systemFeatures.branding.enabled) - src = systemFeatures.branding.login_page_logo - - return ( - <img - src={src} - className={classNames('block w-auto h-10', className)} - alt='logo' - /> - ) -} - -export default LoginLogo diff --git a/web/app/signin/components/social-auth.tsx b/web/app/signin/components/social-auth.tsx index 39d7ceaa40..283c650f23 100644 --- a/web/app/signin/components/social-auth.tsx +++ b/web/app/signin/components/social-auth.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { useSearchParams } from 'next/navigation' import style from '../page.module.css' import Button from '@/app/components/base/button' -import { apiPrefix } from '@/config' +import { API_PREFIX } from '@/config' import classNames from '@/utils/classnames' import { getPurifyHref } from '@/utils' @@ -15,7 +15,7 @@ export default function SocialAuth(props: SocialAuthProps) { const searchParams = useSearchParams() const getOAuthLink = (href: string) => { - const url = getPurifyHref(`${apiPrefix}${href}`) + const url = getPurifyHref(`${API_PREFIX}${href}`) if (searchParams.has('invite_token')) return `${url}?${searchParams.toString()}` @@ -32,7 +32,7 @@ export default function SocialAuth(props: SocialAuthProps) { <span className={ classNames( style.githubIcon, - 'w-5 h-5 mr-2', + 'mr-2 h-5 w-5', ) } /> <span className="truncate">{t('login.withGitHub')}</span> @@ -50,7 +50,7 @@ export default function SocialAuth(props: SocialAuthProps) { <span className={ classNames( style.googleIcon, - 'w-5 h-5 mr-2', + 'mr-2 h-5 w-5', ) } /> <span className="truncate">{t('login.withGoogle')}</span> diff --git a/web/app/signin/components/sso-auth.tsx b/web/app/signin/components/sso-auth.tsx index 960d082157..bb98eb2878 100644 --- a/web/app/signin/components/sso-auth.tsx +++ b/web/app/signin/components/sso-auth.tsx @@ -34,7 +34,7 @@ const SSOAuth: FC<SSOAuthProps> = ({ } else if (protocol === SSOProtocol.OIDC) { getUserOIDCSSOUrl(invite_token).then((res) => { - document.cookie = `user-oidc-state=${res.state}` + document.cookie = `user-oidc-state=${res.state};Path=/` router.push(res.url) }).finally(() => { setIsLoading(false) @@ -42,7 +42,7 @@ const SSOAuth: FC<SSOAuthProps> = ({ } else if (protocol === SSOProtocol.OAuth2) { getUserOAuth2SSOUrl(invite_token).then((res) => { - document.cookie = `user-oauth2-state=${res.state}` + document.cookie = `user-oauth2-state=${res.state};Path=/` router.push(res.url) }).finally(() => { setIsLoading(false) diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 925bc51f16..1ff1c7d671 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,5 +1,6 @@ 'use client' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' import { useCallback, useState } from 'react' import Link from 'next/link' import { useContext } from 'use-context-selector' @@ -15,13 +16,15 @@ import I18n from '@/context/i18n' import { activateMember, invitationCheck } from '@/service/common' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' +import { noop } from 'lodash-es' export default function InviteSettingsPage() { const { t } = useTranslation() + const docLink = useDocLink() const router = useRouter() const searchParams = useSearchParams() const token = decodeURIComponent(searchParams.get('invite_token') as string) - const { locale, setLocaleOnClient } = useContext(I18n) + const { setLocaleOnClient } = useContext(I18n) const [name, setName] = useState('') const [language, setLanguage] = useState(LanguagesSupported[0]) const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles') @@ -86,8 +89,7 @@ export default function InviteSettingsPage() { <div className='pb-4 pt-2'> <h2 className='title-4xl-semi-bold'>{t('login.setYourAccount')}</h2> </div> - <form action=''> - + <form onSubmit={noop}> <div className='mb-5'> <label htmlFor="name" className="system-md-semibold my-2"> {t('login.name')} @@ -99,6 +101,13 @@ export default function InviteSettingsPage() { value={name} onChange={e => setName(e.target.value)} placeholder={t('login.namePlaceholder') || ''} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + handleActivate() + } + }} /> </div> </div> @@ -147,7 +156,7 @@ export default function InviteSettingsPage() { <Link className='system-xs-medium text-text-accent-secondary' target='_blank' rel='noopener noreferrer' - href={`https://docs.dify.ai/${language !== LanguagesSupported[1] ? 'user-agreement' : `v/${locale.toLowerCase()}/policies`}/open-source`} + href={docLink('/policies/open-source')} >{t('login.license.link')}</Link> </div> </div> diff --git a/web/app/signin/oneMoreStep.tsx b/web/app/signin/oneMoreStep.tsx index 7a326a13de..3569cf96d9 100644 --- a/web/app/signin/oneMoreStep.tsx +++ b/web/app/signin/oneMoreStep.tsx @@ -12,6 +12,7 @@ import { timezones } from '@/utils/timezone' import { LanguagesSupported, languages } from '@/i18n/language' import { oneMoreStep } from '@/service/common' import Toast from '@/app/components/base/toast' +import { useDocLink } from '@/context/i18n' type IState = { formState: 'processing' | 'error' | 'success' | 'initial' @@ -51,6 +52,7 @@ const reducer: Reducer<IState, IAction> = (state: IState, action: IAction) => { const OneMoreStep = () => { const { t } = useTranslation() + const docLink = useDocLink() const router = useRouter() const searchParams = useSearchParams() @@ -101,7 +103,6 @@ const OneMoreStep = () => { </div> </div> } - needsDelay > <span className='cursor-pointer text-text-accent-secondary'>{t('login.dontHave')}</span> </Tooltip> @@ -164,7 +165,7 @@ const OneMoreStep = () => { <Link className='system-xs-medium text-text-accent-secondary' target='_blank' rel='noopener noreferrer' - href={'https://docs.dify.ai/en/policies/agreement/README'} + href={docLink('/policies/agreement/README')} >{t('login.license.link')}</Link> </div> </div> diff --git a/web/app/styles/globals.css b/web/app/styles/globals.css index 52e36a2767..353cfa2fff 100644 --- a/web/app/styles/globals.css +++ b/web/app/styles/globals.css @@ -697,4 +697,15 @@ button:focus-within { -ms-overflow-style: none; scrollbar-width: none; } + + /* Hide arrows from number input */ + .no-spinner::-webkit-outer-spin-button, + .no-spinner::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .no-spinner { + -moz-appearance: textfield; + } } diff --git a/web/config/index.spec.ts b/web/config/index.spec.ts new file mode 100644 index 0000000000..de3ee72842 --- /dev/null +++ b/web/config/index.spec.ts @@ -0,0 +1,56 @@ +import { validPassword } from './index' + +describe('validPassword Tests', () => { + const passwordRegex = validPassword + + // Valid passwords + test('Valid passwords: contains letter+digit, length ≥8', () => { + expect(passwordRegex.test('password1')).toBe(true) + expect(passwordRegex.test('PASSWORD1')).toBe(true) + expect(passwordRegex.test('12345678a')).toBe(true) + expect(passwordRegex.test('a1b2c3d4')).toBe(true) + expect(passwordRegex.test('VeryLongPassword123')).toBe(true) + expect(passwordRegex.test('short1')).toBe(false) + }) + + // Missing letter + test('Invalid passwords: missing letter', () => { + expect(passwordRegex.test('12345678')).toBe(false) + expect(passwordRegex.test('!@#$%^&*123')).toBe(false) + }) + + // Missing digit + test('Invalid passwords: missing digit', () => { + expect(passwordRegex.test('password')).toBe(false) + expect(passwordRegex.test('PASSWORD')).toBe(false) + expect(passwordRegex.test('AbCdEfGh')).toBe(false) + }) + + // Too short + test('Invalid passwords: less than 8 characters', () => { + expect(passwordRegex.test('pass1')).toBe(false) + expect(passwordRegex.test('abc123')).toBe(false) + expect(passwordRegex.test('1a')).toBe(false) + }) + + // Boundary test + test('Boundary test: exactly 8 characters', () => { + expect(passwordRegex.test('abc12345')).toBe(true) + expect(passwordRegex.test('1abcdefg')).toBe(true) + }) + + // Special characters + test('Special characters: non-whitespace special chars allowed', () => { + expect(passwordRegex.test('pass@123')).toBe(true) + expect(passwordRegex.test('p@$$w0rd')).toBe(true) + expect(passwordRegex.test('!1aBcDeF')).toBe(true) + }) + + // Contains whitespace + test('Invalid passwords: contains whitespace', () => { + expect(passwordRegex.test('pass word1')).toBe(false) + expect(passwordRegex.test('password1 ')).toBe(false) + expect(passwordRegex.test(' password1')).toBe(false) + expect(passwordRegex.test('pass\tword1')).toBe(false) + }) +}) diff --git a/web/config/index.ts b/web/config/index.ts index 66ccde2aad..667723aaaf 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -1,49 +1,44 @@ import { InputVarType } from '@/app/components/workflow/types' import { AgentStrategy } from '@/types/app' import { PromptRole } from '@/models/debug' +import { DatasetAttr } from '@/types/feature' + +const getBooleanConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr, defaultValue: boolean = true) => { + if (envVar !== undefined && envVar !== '') + return envVar === 'true' + const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) + if (attrValue !== undefined && attrValue !== '') + return attrValue === 'true' + return defaultValue +} -export let apiPrefix = '' -export let publicApiPrefix = '' -export let marketplaceApiPrefix = '' -export let marketplaceUrlPrefix = '' +const getNumberConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr, defaultValue: number) => { + if (envVar) + return Number.parseInt(envVar) -// NEXT_PUBLIC_API_PREFIX=/console/api NEXT_PUBLIC_PUBLIC_API_PREFIX=/api npm run start -if (process.env.NEXT_PUBLIC_API_PREFIX && process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX) { - apiPrefix = process.env.NEXT_PUBLIC_API_PREFIX - publicApiPrefix = process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX -} -else if ( - globalThis.document?.body?.getAttribute('data-api-prefix') - && globalThis.document?.body?.getAttribute('data-pubic-api-prefix') -) { - // Not build can not get env from process.env.NEXT_PUBLIC_ in browser https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser - apiPrefix = globalThis.document.body.getAttribute('data-api-prefix') as string - publicApiPrefix = globalThis.document.body.getAttribute('data-pubic-api-prefix') as string -} -else { - // const domainParts = globalThis.location?.host?.split('.'); - // in production env, the host is dify.app . In other env, the host is [dev].dify.app - // const env = domainParts.length === 2 ? 'ai' : domainParts?.[0]; - apiPrefix = 'http://localhost:5001/console/api' - publicApiPrefix = 'http://localhost:5001/api' // avoid browser private mode api cross origin - marketplaceApiPrefix = 'http://localhost:5002/api' + const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) + if (attrValue) + return Number.parseInt(attrValue) + return defaultValue } -if (process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX && process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX) { - marketplaceApiPrefix = process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX - marketplaceUrlPrefix = process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX -} -else { - marketplaceApiPrefix = globalThis.document?.body?.getAttribute('data-marketplace-api-prefix') || '' - marketplaceUrlPrefix = globalThis.document?.body?.getAttribute('data-marketplace-url-prefix') || '' +const getStringConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr, defaultValue: string) => { + if (envVar) + return envVar + + const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey) + if (attrValue) + return attrValue + return defaultValue } -export const API_PREFIX: string = apiPrefix -export const PUBLIC_API_PREFIX: string = publicApiPrefix -export const MARKETPLACE_API_PREFIX: string = marketplaceApiPrefix -export const MARKETPLACE_URL_PREFIX: string = marketplaceUrlPrefix +export const API_PREFIX = getStringConfig(process.env.NEXT_PUBLIC_API_PREFIX, DatasetAttr.DATA_API_PREFIX, 'http://localhost:5001/console/api') +export const PUBLIC_API_PREFIX = getStringConfig(process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX, DatasetAttr.DATA_PUBLIC_API_PREFIX, 'http://localhost:5001/api') +export const MARKETPLACE_API_PREFIX = getStringConfig(process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX, DatasetAttr.DATA_MARKETPLACE_API_PREFIX, 'http://localhost:5002/api') +export const MARKETPLACE_URL_PREFIX = getStringConfig(process.env.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX, DatasetAttr.DATA_MARKETPLACE_URL_PREFIX, '') + +const EDITION = getStringConfig(process.env.NEXT_PUBLIC_EDITION, DatasetAttr.DATA_PUBLIC_EDITION, 'SELF_HOSTED') -const EDITION = process.env.NEXT_PUBLIC_EDITION || globalThis.document?.body?.getAttribute('data-public-edition') || 'SELF_HOSTED' export const IS_CE_EDITION = EDITION === 'SELF_HOSTED' export const IS_CLOUD_EDITION = EDITION === 'CLOUD' @@ -162,15 +157,6 @@ export const ANNOTATION_DEFAULT = { score_threshold: 0.9, } -export let maxToolsNum = 10 - -if (process.env.NEXT_PUBLIC_MAX_TOOLS_NUM && process.env.NEXT_PUBLIC_MAX_TOOLS_NUM !== '') - maxToolsNum = Number.parseInt(process.env.NEXT_PUBLIC_MAX_TOOLS_NUM) -else if (globalThis.document?.body?.getAttribute('data-public-max-tools-num') && globalThis.document.body.getAttribute('data-public-max-tools-num') !== '') - maxToolsNum = Number.parseInt(globalThis.document.body.getAttribute('data-public-max-tools-num') as string) - -export const MAX_TOOLS_NUM = maxToolsNum - export const DEFAULT_AGENT_SETTING = { enabled: false, max_iteration: 10, @@ -269,15 +255,6 @@ export const VAR_REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_]\w{0,29}){1,10}#) export const resetReg = () => VAR_REGEX.lastIndex = 0 -export let textGenerationTimeoutMs = 60000 - -if (process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS && process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS !== '') - textGenerationTimeoutMs = Number.parseInt(process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS) -else if (globalThis.document?.body?.getAttribute('data-public-text-generation-timeout-ms') && globalThis.document.body.getAttribute('data-public-text-generation-timeout-ms') !== '') - textGenerationTimeoutMs = Number.parseInt(globalThis.document.body.getAttribute('data-public-text-generation-timeout-ms') as string) - -export const TEXT_GENERATION_TIMEOUT_MS = textGenerationTimeoutMs - export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || '' @@ -286,32 +263,18 @@ export const SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS = '.difypkg,.difybndl' export const FULL_DOC_PREVIEW_LENGTH = 50 export const JSON_SCHEMA_MAX_DEPTH = 10 -let loopNodeMaxCount = 100 - -if (process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT && process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT !== '') - loopNodeMaxCount = Number.parseInt(process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT) -else if (globalThis.document?.body?.getAttribute('data-public-loop-node-max-count') && globalThis.document.body.getAttribute('data-public-loop-node-max-count') !== '') - loopNodeMaxCount = Number.parseInt(globalThis.document.body.getAttribute('data-public-loop-node-max-count') as string) - -export const LOOP_NODE_MAX_COUNT = loopNodeMaxCount - -let maxIterationsNum = 99 - -if (process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM && process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM !== '') - maxIterationsNum = Number.parseInt(process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM) -else if (globalThis.document?.body?.getAttribute('data-public-max-iterations-num') && globalThis.document.body.getAttribute('data-public-max-iterations-num') !== '') - maxIterationsNum = Number.parseInt(globalThis.document.body.getAttribute('data-public-max-iterations-num') as string) -export const MAX_ITERATIONS_NUM = maxIterationsNum +export const MAX_TOOLS_NUM = getNumberConfig(process.env.NEXT_PUBLIC_MAX_TOOLS_NUM, DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM, 10) +export const TEXT_GENERATION_TIMEOUT_MS = getNumberConfig(process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, 60000) +export const LOOP_NODE_MAX_COUNT = getNumberConfig(process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT, DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT, 100) +export const MAX_ITERATIONS_NUM = getNumberConfig(process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM, 99) +export const MAX_TREE_DEPTH = getNumberConfig(process.env.NEXT_PUBLIC_MAX_TREE_DEPTH, DatasetAttr.DATA_PUBLIC_MAX_TREE_DEPTH, 50) -export const ENABLE_WEBSITE_JINAREADER = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER !== undefined - ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER === 'true' - : globalThis.document?.body?.getAttribute('data-public-enable-website-jinareader') === 'true' || true +export const ALLOW_UNSAFE_DATA_SCHEME = getBooleanConfig(process.env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, DatasetAttr.DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME, false) +export const ENABLE_WEBSITE_JINAREADER = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER, true) +export const ENABLE_WEBSITE_FIRECRAWL = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL, true) +export const ENABLE_WEBSITE_WATERCRAWL = getBooleanConfig(process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, DatasetAttr.DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL, false) -export const ENABLE_WEBSITE_FIRECRAWL = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL !== undefined - ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL === 'true' - : globalThis.document?.body?.getAttribute('data-public-enable-website-firecrawl') === 'true' || true +export const VALUE_SELECTOR_DELIMITER = '@@@' -export const ENABLE_WEBSITE_WATERCRAWL = process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL !== undefined - ? process.env.NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL === 'true' - : globalThis.document?.body?.getAttribute('data-public-enable-website-watercrawl') === 'true' || true +export const validPassword = /^(?=.*[a-zA-Z])(?=.*\d)\S{8,}$/ diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index 47710c8fc1..cb737c5288 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -102,6 +102,7 @@ type IDebugConfiguration = { setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void isAllowVideoUpload: boolean isShowDocumentConfig: boolean + isShowAudioConfig: boolean rerankSettingModalOpen: boolean setRerankSettingModalOpen: (rerankSettingModalOpen: boolean) => void } @@ -254,6 +255,7 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({ setVisionConfig: noop, isAllowVideoUpload: false, isShowDocumentConfig: false, + isShowAudioConfig: false, rerankSettingModalOpen: false, setRerankSettingModalOpen: noop, }) diff --git a/web/context/i18n.ts b/web/context/i18n.ts index 463e01d1c5..ef53a4b481 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -24,15 +24,23 @@ export const useGetLanguage = () => { return getLanguage(locale) } -export const useGetDocLanguage = () => { - const { locale } = useI18N() - - return getDocLanguage(locale) -} export const useGetPricingPageLanguage = () => { const { locale } = useI18N() return getPricingPageLanguage(locale) } +const defaultDocBaseUrl = 'https://docs.dify.ai' +export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => { + let baseDocUrl = baseUrl || defaultDocBaseUrl + baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl + const { locale } = useI18N() + const docLanguage = getDocLanguage(locale) + return (path?: string, pathMap?: { [index: string]: string }): string => { + const pathUrl = path || '' + let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl + targetPath = (targetPath.startsWith('/')) ? targetPath.slice(1) : targetPath + return `${baseDocUrl}/${docLanguage}/${targetPath}` + } +} export default I18NContext diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 70c9019aca..aaec900fd8 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -154,10 +154,6 @@ export const ProviderContextProvider = ({ setIsFetchedPlan(true) } - if (data.model_load_balancing_enabled) - setModelLoadBalancingEnabled(true) - if (data.dataset_operator_enabled) - setDatasetOperatorEnabled(true) if (data.model_load_balancing_enabled) setModelLoadBalancingEnabled(true) if (data.dataset_operator_enabled) diff --git a/web/docker/entrypoint.sh b/web/docker/entrypoint.sh index 59e9c472b3..ef13011a71 100755 --- a/web/docker/entrypoint.sh +++ b/web/docker/entrypoint.sh @@ -26,6 +26,7 @@ export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED} export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS} export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST} export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED} +export NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=${ALLOW_UNSAFE_DATA_SCHEME:-false} export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE} export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH} export NEXT_PUBLIC_MAX_TOOLS_NUM=${MAX_TOOLS_NUM} @@ -35,4 +36,5 @@ export NEXT_PUBLIC_ENABLE_WEBSITE_WATERCRAWL=${ENABLE_WEBSITE_WATERCRAWL:-true} export NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=${LOOP_NODE_MAX_COUNT} export NEXT_PUBLIC_MAX_PARALLEL_LIMIT=${MAX_PARALLEL_LIMIT} export NEXT_PUBLIC_MAX_ITERATIONS_NUM=${MAX_ITERATIONS_NUM} +export NEXT_PUBLIC_MAX_TREE_DEPTH=${MAX_TREE_DEPTH} pm2 start /app/web/server.js --name dify-web --cwd /app/web -i ${PM2_INSTANCES} --no-daemon diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index b2696f7561..ef87a7883e 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -213,7 +213,7 @@ export default combine( settings: { tailwindcss: { // These are the default values but feel free to customize - callees: ['classnames', 'clsx', 'ctl', 'cn'], + callees: ['classnames', 'clsx', 'ctl', 'cn', 'classNames'], config: 'tailwind.config.js', // returned from `loadConfig()` utility if not provided cssFiles: [ '**/*.css', diff --git a/web/hooks/use-oauth.ts b/web/hooks/use-oauth.ts new file mode 100644 index 0000000000..ae9c1cda66 --- /dev/null +++ b/web/hooks/use-oauth.ts @@ -0,0 +1,36 @@ +'use client' +import { useEffect } from 'react' + +export const useOAuthCallback = () => { + useEffect(() => { + if (window.opener) { + window.opener.postMessage({ + type: 'oauth_callback', + }, '*') + window.close() + } + }, []) +} + +export const openOAuthPopup = (url: string, callback: () => void) => { + const width = 600 + const height = 600 + const left = window.screenX + (window.outerWidth - width) / 2 + const top = window.screenY + (window.outerHeight - height) / 2 + + const popup = window.open( + url, + 'OAuth', + `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`, + ) + + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === 'oauth_callback') { + window.removeEventListener('message', handleMessage) + callback() + } + } + + window.addEventListener('message', handleMessage) + return popup +} diff --git a/web/hooks/use-tab-searchparams.ts b/web/hooks/use-tab-searchparams.ts index 0c0e3b7773..444944f812 100644 --- a/web/hooks/use-tab-searchparams.ts +++ b/web/hooks/use-tab-searchparams.ts @@ -29,9 +29,10 @@ export const useTabSearchParams = ({ const router = useRouter() const pathName = pathnameFromHook || window?.location?.pathname const searchParams = useSearchParams() + const searchParamValue = searchParams.has(searchParamName) ? decodeURIComponent(searchParams.get(searchParamName)!) : defaultTab const [activeTab, setTab] = useState<string>( !disableSearchParams - ? (searchParams.get(searchParamName) || defaultTab) + ? searchParamValue : defaultTab, ) @@ -39,7 +40,7 @@ export const useTabSearchParams = ({ setTab(newActiveTab) if (disableSearchParams) return - router[`${routingBehavior}`](`${pathName}?${searchParamName}=${newActiveTab}`) + router[`${routingBehavior}`](`${pathName}?${searchParamName}=${encodeURIComponent(newActiveTab)}`) } return [activeTab, setActiveTab] as const diff --git a/web/i18n/de-DE/app-debug.ts b/web/i18n/de-DE/app-debug.ts index 4022e755e9..1adaf6f05d 100644 --- a/web/i18n/de-DE/app-debug.ts +++ b/web/i18n/de-DE/app-debug.ts @@ -298,6 +298,7 @@ const translation = { add: 'Hinzufügen', writeOpener: 'Eröffnung schreiben', placeholder: 'Schreiben Sie hier Ihre Eröffnungsnachricht, Sie können Variablen verwenden, versuchen Sie {{Variable}} zu tippen.', + openingQuestionPlaceholder: 'Sie können Variablen verwenden, versuchen Sie {{variable}} einzugeben.', openingQuestion: 'Eröffnungsfragen', noDataPlaceHolder: 'Den Dialog mit dem Benutzer zu beginnen, kann helfen, in konversationellen Anwendungen eine engere Verbindung mit ihnen herzustellen.', diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index 1373dd611b..52819d0c7e 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -95,6 +95,7 @@ const translation = { foundResult: '{{Anzahl}} Ergebnis', agentUserDescription: 'Ein intelligenter Agent, der in der Lage ist, iteratives Denken zu führen und autonome Werkzeuge zu verwenden, um Aufgabenziele zu erreichen.', agentShortDescription: 'Intelligenter Agent mit logischem Denken und autonomer Werkzeugnutzung', + dropDSLToCreateApp: 'Ziehen Sie die DSL-Datei hierher, um die App zu erstellen', }, editApp: 'App bearbeiten', editAppTitle: 'App-Informationen bearbeiten', @@ -137,6 +138,14 @@ const translation = { notConfigured: 'Anbieter konfigurieren, um Nachverfolgung zu aktivieren', moreProvider: 'Weitere Anbieter', }, + arize: { + title: 'Arize', + description: 'Unternehmensgerechte LLM-Observierbarkeit, Online- und Offline-Bewertung, Überwachung und Experimentierung—unterstützt durch OpenTelemetry. Speziell für LLM- und agentenbasierte Anwendungen entwickelt.', + }, + phoenix: { + title: 'Phoenix', + description: 'Open-Source- und OpenTelemetry-basierte Plattform für Observierbarkeit, Bewertung, Prompt-Engineering und Experimentierung für Ihre LLM-Workflows und -Agenten.', + }, langsmith: { title: 'LangSmith', description: 'Eine All-in-One-Entwicklerplattform für jeden Schritt des LLM-gesteuerten Anwendungslebenszyklus.', @@ -165,6 +174,7 @@ const translation = { title: 'Weben', description: 'Weave ist eine Open-Source-Plattform zur Bewertung, Testung und Überwachung von LLM-Anwendungen.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', diff --git a/web/i18n/de-DE/common.ts b/web/i18n/de-DE/common.ts index 3e00cfcaed..5f4682b2a1 100644 --- a/web/i18n/de-DE/common.ts +++ b/web/i18n/de-DE/common.ts @@ -58,6 +58,8 @@ const translation = { downloadSuccess: 'Download abgeschlossen.', more: 'Mehr', format: 'Format', + selectAll: 'Alles auswählen', + deSelectAll: 'Alle abwählen', }, placeholder: { input: 'Bitte eingeben', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'API-Erweiterungen bieten zentralisiertes API-Management und vereinfachen die Konfiguration für eine einfache Verwendung in Difys Anwendungen.', link: 'Erfahren Sie, wie Sie Ihre eigene API-Erweiterung entwickeln.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'API-Erweiterung hinzufügen', selector: { title: 'API-Erweiterung', diff --git a/web/i18n/de-DE/dataset-creation.ts b/web/i18n/de-DE/dataset-creation.ts index 9500c0cd68..a34533806b 100644 --- a/web/i18n/de-DE/dataset-creation.ts +++ b/web/i18n/de-DE/dataset-creation.ts @@ -69,7 +69,6 @@ const translation = { unknownError: 'Unbekannter Fehler', resetAll: 'Alles zurücksetzen', extractOnlyMainContent: 'Extrahieren Sie nur den Hauptinhalt (keine Kopf-, Navigations- und Fußzeilen usw.)', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', firecrawlTitle: 'Extrahieren von Webinhalten mit 🔥Firecrawl', maxDepthTooltip: 'Maximale Tiefe für das Crawlen relativ zur eingegebenen URL. Tiefe 0 kratzt nur die Seite der eingegebenen URL, Tiefe 1 kratzt die URL und alles nach der eingegebenen URL + ein / und so weiter.', crawlSubPage: 'Unterseiten crawlen', @@ -85,7 +84,6 @@ const translation = { configureJinaReader: 'Jina Reader konfigurieren', waterCrawlNotConfigured: 'Watercrawl ist nicht konfiguriert', configureWatercrawl: 'Wasserkrabbe konfigurieren', - watercrawlDocLink: 'https://docs.dify.ai/de/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', watercrawlTitle: 'Webinhalt mit Watercrawl extrahieren', watercrawlDoc: 'Wasserkriechen-Dokumente', configureFirecrawl: 'Firecrawl konfigurieren', diff --git a/web/i18n/de-DE/dataset-documents.ts b/web/i18n/de-DE/dataset-documents.ts index 22018f9da4..baf2e69c74 100644 --- a/web/i18n/de-DE/dataset-documents.ts +++ b/web/i18n/de-DE/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Löschen', enableWarning: 'Archivierte Datei kann nicht aktiviert werden', sync: 'Synchronisieren', + resume: 'Fortsetzen', + pause: 'Pause', }, index: { enable: 'Aktivieren', @@ -390,6 +392,8 @@ const translation = { addChildChunk: 'Untergeordneten Block hinzufügen', regenerationConfirmTitle: 'Möchten Sie untergeordnete Chunks regenerieren?', searchResults_one: 'ERGEBNIS', + keywordEmpty: 'Das Schlüsselwort darf nicht leer sein.', + keywordDuplicate: 'Das Schlüsselwort existiert bereits', }, } diff --git a/web/i18n/de-DE/dataset.ts b/web/i18n/de-DE/dataset.ts index 4d513aab1b..3d2d924bac 100644 --- a/web/i18n/de-DE/dataset.ts +++ b/web/i18n/de-DE/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'Der Metadatenname darf nicht leer sein.', invalid: 'Der Metadatenname darf nur Kleinbuchstaben, Zahlen und Unterstriche enthalten und muss mit einem Kleinbuchstaben beginnen.', + tooLong: 'Der Metadatenname darf {{max}} Zeichen nicht überschreiten.', }, batchEditMetadata: { editMetadata: 'Metadaten bearbeiten', diff --git a/web/i18n/de-DE/plugin.ts b/web/i18n/de-DE/plugin.ts index 498ac86573..87f222be94 100644 --- a/web/i18n/de-DE/plugin.ts +++ b/web/i18n/de-DE/plugin.ts @@ -55,7 +55,7 @@ const translation = { unsupportedContent: 'Die installierte Plug-in-Version bietet diese Aktion nicht.', unsupportedTitle: 'Nicht unterstützte Aktion', descriptionPlaceholder: 'Kurze Beschreibung des Zwecks des Werkzeugs, z. B. um die Temperatur für einen bestimmten Ort zu ermitteln.', - auto: 'Automatisch', + auto: 'Auto', params: 'KONFIGURATION DER ARGUMENTATION', unsupportedContent2: 'Klicken Sie hier, um die Version zu wechseln.', placeholder: 'Wählen Sie ein Werkzeug aus...', @@ -137,6 +137,7 @@ const translation = { readyToInstall: 'Über die Installation des folgenden Plugins', dropPluginToInstall: 'Legen Sie das Plugin-Paket hier ab, um es zu installieren', next: 'Nächster', + installWarning: 'Dieses Plugin darf nicht installiert werden.', }, installFromGitHub: { selectPackagePlaceholder: 'Bitte wählen Sie ein Paket aus', @@ -173,7 +174,7 @@ const translation = { recentlyUpdated: 'Kürzlich aktualisiert', }, viewMore: 'Mehr anzeigen', - sortBy: 'Schwarze Stadt', + sortBy: 'Sortieren nach', discover: 'Entdecken', noPluginFound: 'Kein Plugin gefunden', difyMarketplace: 'Dify Marktplatz', @@ -195,7 +196,6 @@ const translation = { allCategories: 'Alle Kategorien', install: '{{num}} Installationen', installAction: 'Installieren', - submitPlugin: 'Plugin einreichen', from: 'Von', fromMarketplace: 'Aus dem Marketplace', search: 'Suchen', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Die aktuelle Dify-Version ist mit diesem Plugin nicht kompatibel, bitte aktualisieren Sie auf die erforderliche Mindestversion: {{minimalDifyVersion}}', requestAPlugin: 'Ein Plugin anfordern', + publishPlugins: 'Plugins veröffentlichen', } export default translation diff --git a/web/i18n/de-DE/tools.ts b/web/i18n/de-DE/tools.ts index 2f3c24b9da..6e6eda85e0 100644 --- a/web/i18n/de-DE/tools.ts +++ b/web/i18n/de-DE/tools.ts @@ -137,21 +137,97 @@ const translation = { notAuthorized: 'Werkzeug nicht autorisiert', howToGet: 'Wie erhält man', addToolModal: { + type: 'Art', + category: 'Kategorie', + add: 'hinzufügen', added: 'zugefügt', manageInTools: 'Verwalten in Tools', - add: 'hinzufügen', - category: 'Kategorie', - emptyTitle: 'Kein Workflow-Tool verfügbar', - type: 'Art', - emptyTip: 'Gehen Sie zu "Workflow -> Als Tool veröffentlichen"', - emptyTitleCustom: 'Kein benutzerdefiniertes Tool verfügbar', - emptyTipCustom: 'Erstellen eines benutzerdefinierten Werkzeugs', + custom: { + title: 'Kein benutzerdefiniertes Werkzeug verfügbar', + tip: 'Benutzerdefiniertes Werkzeug erstellen', + }, + workflow: { + title: 'Kein Workflow-Werkzeug verfügbar', + tip: 'Veröffentlichen Sie Workflows als Werkzeuge im Studio', + }, + mcp: { + title: 'Kein MCP-Werkzeug verfügbar', + tip: 'Einen MCP-Server hinzufügen', + }, + agent: { + title: 'Keine Agentenstrategie verfügbar', + }, }, toolNameUsageTip: 'Name des Tool-Aufrufs für die Argumentation und Aufforderung des Agenten', customToolTip: 'Erfahren Sie mehr über benutzerdefinierte Dify-Tools', openInStudio: 'In Studio öffnen', noTools: 'Keine Werkzeuge gefunden', copyToolName: 'Name kopieren', + mcp: { + create: { + cardTitle: 'MCP-Server hinzufügen (HTTP)', + cardLink: 'Mehr über MCP-Server-Integration erfahren', + }, + noConfigured: 'Nicht konfigurierter Server', + updateTime: 'Aktualisiert', + toolsCount: '{{count}} Tools', + noTools: 'Keine Tools verfügbar', + modal: { + title: 'MCP-Server hinzufügen (HTTP)', + editTitle: 'MCP-Server bearbeiten (HTTP)', + name: 'Name & Symbol', + namePlaceholder: 'Benennen Sie Ihren MCP-Server', + serverUrl: 'Server-URL', + serverUrlPlaceholder: 'URL zum Server-Endpunkt', + serverUrlWarning: 'Das Ändern der Serveradresse kann Anwendungen unterbrechen, die von diesem Server abhängen', + serverIdentifier: 'Serverkennung', + serverIdentifierTip: 'Eindeutige Kennung für den MCP-Server im Arbeitsbereich. Nur Kleinbuchstaben, Zahlen, Unterstriche und Bindestriche. Maximal 24 Zeichen.', + serverIdentifierPlaceholder: 'Eindeutige Kennung, z.B. mein-mcp-server', + serverIdentifierWarning: 'Nach einer ID-Änderung wird der Server von vorhandenen Apps nicht erkannt', + cancel: 'Abbrechen', + save: 'Speichern', + confirm: 'Hinzufügen & Autorisieren', + }, + delete: 'MCP-Server entfernen', + deleteConfirmTitle: 'Möchten Sie {{mcp}} entfernen?', + operation: { + edit: 'Bearbeiten', + remove: 'Entfernen', + }, + authorize: 'Autorisieren', + authorizing: 'Wird autorisiert...', + authorizingRequired: 'Autorisierung erforderlich', + authorizeTip: 'Nach der Autorisierung werden Tools hier angezeigt.', + update: 'Aktualisieren', + updating: 'Wird aktualisiert', + gettingTools: 'Tools werden abgerufen...', + updateTools: 'Tools werden aktualisiert...', + toolsEmpty: 'Tools nicht geladen', + getTools: 'Tools abrufen', + toolUpdateConfirmTitle: 'Tool-Liste aktualisieren', + toolUpdateConfirmContent: 'Das Aktualisieren der Tool-Liste kann bestehende Apps beeinflussen. Fortfahren?', + toolsNum: '{{count}} Tools enthalten', + onlyTool: '1 Tool enthalten', + identifier: 'Serverkennung (Zum Kopieren klicken)', + server: { + title: 'MCP-Server', + url: 'Server-URL', + reGen: 'Server-URL neu generieren?', + addDescription: 'Beschreibung hinzufügen', + edit: 'Beschreibung bearbeiten', + modal: { + addTitle: 'Beschreibung hinzufügen, um MCP-Server zu aktivieren', + editTitle: 'Beschreibung bearbeiten', + description: 'Beschreibung', + descriptionPlaceholder: 'Erklären Sie, was dieses Tool tut und wie es vom LLM verwendet werden soll', + parameters: 'Parameter', + parametersTip: 'Fügen Sie Beschreibungen für jeden Parameter hinzu, um dem LLM Zweck und Einschränkungen zu verdeutlichen.', + parametersPlaceholder: 'Zweck und Einschränkungen des Parameters', + confirm: 'MCP-Server aktivieren', + }, + publishTip: 'App nicht veröffentlicht. Bitte zuerst die App veröffentlichen.', + }, + }, } export default translation diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 1eb7804271..ba49f72b69 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Variable setzen', needConnectTip: 'Dieser Schritt ist mit nichts verbunden', maxTreeDepth: 'Maximales Limit von {{depth}} Knoten pro Ast', - needEndNode: 'Der Endblock muss hinzugefügt werden', - needAnswerNode: 'Der Antwortblock muss hinzugefügt werden', workflowProcess: 'Arbeitsablauf', notRunning: 'Noch nicht ausgeführt', previewPlaceholder: 'Geben Sie den Inhalt in das Feld unten ein, um das Debuggen des Chatbots zu starten', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Mehr erfahren', copy: 'Kopieren', duplicate: 'Duplizieren', - addBlock: 'Block hinzufügen', pasteHere: 'Hier einfügen', pointerMode: 'Zeigermodus', handMode: 'Handmodus', @@ -115,6 +112,9 @@ const translation = { exportJPEG: 'Als JPEG exportieren', exitVersions: 'Ausgangsversionen', exportPNG: 'Als PNG exportieren', + addBlock: 'Knoten hinzufügen', + needEndNode: 'Der Endknoten muss hinzugefügt werden.', + needAnswerNode: 'Der Antwortknoten muss hinzugefügt werden.', }, env: { envPanelTitle: 'Umgebungsvariablen', @@ -129,6 +129,8 @@ const translation = { value: 'Wert', valuePlaceholder: 'Umgebungswert', secretTip: 'Wird verwendet, um sensible Informationen oder Daten zu definieren, wobei DSL-Einstellungen zur Verhinderung von Lecks konfiguriert sind.', + description: 'Beschreibung', + descriptionPlaceholder: 'Beschreiben Sie die Variable', }, export: { title: 'Geheime Umgebungsvariablen exportieren?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} Schritte vorwärts', sessionStart: 'Sitzungsstart', currentState: 'Aktueller Zustand', - nodeTitleChange: 'Blocktitel geändert', - nodeDescriptionChange: 'Blockbeschreibung geändert', - nodeDragStop: 'Block verschoben', - nodeChange: 'Block geändert', - nodeConnect: 'Block verbunden', - nodePaste: 'Block eingefügt', - nodeDelete: 'Block gelöscht', - nodeAdd: 'Block hinzugefügt', - nodeResize: 'Blockgröße geändert', noteAdd: 'Notiz hinzugefügt', noteChange: 'Notiz geändert', noteDelete: 'Notiz gelöscht', - edgeDelete: 'Block getrennt', + edgeDelete: 'Knoten getrennt', + nodeAdd: 'Knoten hinzugefügt', + nodeTitleChange: 'Knotenüberschrift geändert', + nodePaste: 'Knoten eingefügt', + nodeResize: 'Knoten verkleinert', + nodeDescriptionChange: 'Die Knotenbeschreibung wurde geändert', + nodeChange: 'Knoten geändert', + nodeConnect: 'Node verbunden', + nodeDragStop: 'Knoten verschoben', + nodeDelete: 'Knoten gelöscht', }, errorMsg: { fieldRequired: '{{field}} ist erforderlich', @@ -217,8 +219,6 @@ const translation = { loop: 'Schleife', }, tabs: { - 'searchBlock': 'Block suchen', - 'blocks': 'Blöcke', 'tools': 'Werkzeuge', 'allTool': 'Alle', 'builtInTool': 'Eingebaut', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Suchwerkzeug', 'plugin': 'Stecker', 'agent': 'Agenten-Strategie', + 'searchBlock': 'Suchknoten', + 'blocks': 'Knoten', }, blocks: { 'start': 'Start', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Benutzereingabefeld', - changeBlock: 'Block ändern', helpLink: 'Hilfelink', about: 'Über', createdBy: 'Erstellt von ', nextStep: 'Nächster Schritt', - addNextStep: 'Fügen Sie den nächsten Block in diesem Workflow hinzu', - selectNextStep: 'Nächsten Block auswählen', runThisStep: 'Diesen Schritt ausführen', checklist: 'Checkliste', checklistTip: 'Stellen Sie sicher, dass alle Probleme vor der Veröffentlichung gelöst sind', checklistResolved: 'Alle Probleme wurden gelöst', - organizeBlocks: 'Blöcke organisieren', change: 'Ändern', optional: '(optional)', moveToThisNode: 'Bewege zu diesem Knoten', + selectNextStep: 'Nächsten Schritt auswählen', + addNextStep: 'Fügen Sie den nächsten Schritt in diesem Arbeitsablauf hinzu.', + organizeBlocks: 'Knoten organisieren', + changeBlock: 'Knoten ändern', + maximize: 'Maximiere die Leinwand', + minimize: 'Vollbildmodus beenden', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { ms: 'Frau', retries: '{{num}} Wiederholungen', }, + typeSwitch: {}, }, start: { required: 'erforderlich', @@ -535,6 +540,10 @@ const translation = { title: 'Importieren von cURL', placeholder: 'Fügen Sie hier die cURL-Zeichenfolge ein', }, + verifySSL: { + title: 'SSL-Zertifikat überprüfen', + warningTooltip: 'Das Deaktivieren der SSL-Überprüfung wird für Produktionsumgebungen nicht empfohlen. Dies sollte nur in der Entwicklung oder im Test verwendet werden, da es die Verbindung anfällig für Sicherheitsbedrohungen wie Man-in-the-Middle-Angriffe macht.', + }, }, code: { inputVars: 'Eingabevariablen', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Eingabevariablen', outputVars: { className: 'Klassennamen', + usage: 'Nutzungsinformationen des Modells', }, class: 'Klasse', classNamePlaceholder: 'Geben Sie Ihren Klassennamen ein', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Eingabevariable', + outputVars: { + isSuccess: 'Ist Erfolg. Bei Erfolg beträgt der Wert 1, bei Misserfolg beträgt der Wert 0.', + errorReason: 'Fehlergrund', + usage: 'Nutzungsinformationen des Modells', + }, extractParameters: 'Parameter extrahieren', importFromTool: 'Aus Tools importieren', addExtractParameter: 'Extraktionsparameter hinzufügen', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Erweiterte Einstellung', reasoningMode: 'Schlussfolgerungsmodus', reasoningModeTip: 'Sie können den entsprechenden Schlussfolgerungsmodus basierend auf der Fähigkeit des Modells wählen, auf Anweisungen zur Funktionsaufruf- oder Eingabeaufforderungen zu reagieren.', - isSuccess: 'Ist Erfolg. Bei Erfolg beträgt der Wert 1, bei Misserfolg beträgt der Wert 0.', - errorReason: 'Fehlergrund', }, iteration: { deleteTitle: 'Iterationsknoten löschen?', @@ -917,6 +930,35 @@ const translation = { deletionTip: 'Die Löschung ist unumkehrbar, bitte bestätigen Sie.', restorationTip: 'Nach der Wiederherstellung der Version wird der aktuelle Entwurf überschrieben.', }, + debug: { + noData: { + runThisNode: 'Führe diesen Knoten aus', + description: 'Die Ergebnisse des letzten Laufs werden hier angezeigt.', + }, + variableInspect: { + trigger: { + normal: 'Variable untersuchen', + stop: 'Halt an', + running: 'Caching-Betriebsstatus', + clear: 'Klar', + cached: 'Cached-Variablen anzeigen', + }, + title: 'Variable untersuchen', + clearAll: 'Alles zurücksetzen', + emptyLink: 'Erfahren Sie mehr', + view: 'Protokoll anzeigen', + systemNode: 'System', + edited: 'Bearbeitet', + clearNode: 'Cache-Variable löschen', + envNode: 'Umwelt', + chatNode: 'Gespräch', + resetConversationVar: 'Setze die Gesprächsvariable auf den Standardwert zurück', + reset: 'Auf den letzten Ausführungswert zurücksetzen', + emptyTip: 'Nachdem Sie einen Knoten auf der Leinwand durchlaufen oder einen Knoten Schritt für Schritt ausgeführt haben, können Sie den aktuellen Wert der Knotenvariable in der Variableninspektion anzeigen.', + }, + settingsTab: 'Einstellungen', + lastRunTab: 'Letzte Ausführung', + }, } export default translation diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 349ff37118..938cb27c12 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -222,6 +222,10 @@ const translation = { title: 'Document', description: 'Enable Document will allows the model to take in documents and answer questions about them.', }, + audioUpload: { + title: 'Audio', + description: 'Enable Audio will allow the model to process audio files for transcription and analysis.', + }, }, codegen: { title: 'Code Generator', @@ -442,6 +446,7 @@ const translation = { writeOpener: 'Edit opener', placeholder: 'Write your opener message here, you can use variables, try type {{variable}}.', openingQuestion: 'Opening Questions', + openingQuestionPlaceholder: 'You can use variables, try typing {{variable}}.', noDataPlaceHolder: 'Starting the conversation with the user can help AI establish a closer connection with them in conversational applications.', varTip: 'You can use variables, try type {{variable}}', diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index ccfe23ead6..8ddb0f1bfe 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -87,6 +87,7 @@ const translation = { appCreateDSLErrorPart3: 'Current application DSL version: ', appCreateDSLErrorPart4: 'System-supported DSL version: ', appCreateFailed: 'Failed to create app', + dropDSLToCreateApp: 'Drop DSL file here to create app', }, newAppFromTemplate: { byCategories: 'BY CATEGORIES', @@ -149,6 +150,14 @@ const translation = { notConfigured: 'Config provider to enable tracing', moreProvider: 'More Provider', }, + arize: { + title: 'Arize', + description: 'Enterprise-grade LLM observability, online & offline evaluation, monitoring, and experimentation—powered by OpenTelemetry. Purpose-built for LLM & agent-driven applications.', + }, + phoenix: { + title: 'Phoenix', + description: 'Open-source & OpenTelemetry-based observability, evaluation, prompt engineering and experimentation platform for your LLM workflows and agents.', + }, langsmith: { title: 'LangSmith', description: 'An all-in-one developer platform for every step of the LLM-powered application lifecycle.', @@ -165,6 +174,10 @@ const translation = { title: 'Weave', description: 'Weave is an open-source platform for evaluating, testing, and monitoring LLM applications.', }, + aliyun: { + title: 'Cloud Monitor', + description: 'The fully-managed and maintenance-free observability platform provided by Alibaba Cloud, enables out-of-the-box monitoring, tracing, and evaluation of Dify applications.', + }, inUse: 'In use', configProvider: { title: 'Config ', @@ -198,9 +211,9 @@ const translation = { accessControl: 'Web App Access Control', accessItemsDescription: { anyone: 'Anyone can access the web app (no login required)', - specific: 'Only specific members within the platform can access the Web application', - organization: 'All members within the platform can access the Web application', - external: 'Only authenticated external users can access the Web application', + specific: 'Only specific members within the platform can access the web app', + organization: 'All members within the platform can access the web app', + external: 'Only authenticated external users can access the web app', }, accessControlDialog: { title: 'Web App Access Control', @@ -217,7 +230,7 @@ const translation = { members_one: '{{count}} MEMBER', members_other: '{{count}} MEMBERS', noGroupsOrMembers: 'No groups or members selected', - webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the Web application.', + webAppSSONotEnabledTip: 'Please contact your organization administrator to configure external authentication for the web app.', operateGroupAndMember: { searchPlaceholder: 'Search groups and members', allMembers: 'All members', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 05000f9343..0823c6129c 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -64,6 +64,8 @@ const translation = { skip: 'Skip', format: 'Format', more: 'More', + selectAll: 'Select All', + deSelectAll: 'Deselect All', }, errorMsg: { fieldRequired: '{{field}} is required', @@ -454,6 +456,7 @@ const translation = { connected: 'Connected', disconnected: 'Disconnected', changeAuthorizedPages: 'Change authorized pages', + integratedAlert: 'Notion is integrated via internal credential, no need to re-authorize.', pagesAuthorized: 'Pages authorized', sync: 'Sync', remove: 'Remove', @@ -484,7 +487,6 @@ const translation = { apiBasedExtension: { title: 'API extensions provide centralized API management, simplifying configuration for easy use across Dify\'s applications.', link: 'Learn how to develop your own API Extension.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Add API Extension', selector: { title: 'API Extension', diff --git a/web/i18n/en-US/dataset-creation.ts b/web/i18n/en-US/dataset-creation.ts index cf2d454f06..90bac8dcb8 100644 --- a/web/i18n/en-US/dataset-creation.ts +++ b/web/i18n/en-US/dataset-creation.ts @@ -80,10 +80,8 @@ const translation = { run: 'Run', firecrawlTitle: 'Extract web content with 🔥Firecrawl', firecrawlDoc: 'Firecrawl docs', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', watercrawlTitle: 'Extract web content with Watercrawl', watercrawlDoc: 'Watercrawl docs', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', jinaReaderTitle: 'Convert the entire site to Markdown', jinaReaderDoc: 'Learn more about Jina Reader', jinaReaderDocLink: 'https://jina.ai/reader', diff --git a/web/i18n/en-US/dataset-documents.ts b/web/i18n/en-US/dataset-documents.ts index 2a79324477..9bcefd6358 100644 --- a/web/i18n/en-US/dataset-documents.ts +++ b/web/i18n/en-US/dataset-documents.ts @@ -30,6 +30,8 @@ const translation = { delete: 'Delete', enableWarning: 'Archived file cannot be enabled', sync: 'Sync', + pause: 'Pause', + resume: 'Resume', }, index: { enable: 'Enable', @@ -355,7 +357,9 @@ const translation = { newChildChunk: 'New Child Chunk', keywords: 'KEYWORDS', addKeyWord: 'Add keyword', + keywordEmpty: 'The keyword cannot be empty', keywordError: 'The maximum length of keyword is 20', + keywordDuplicate: 'The keyword already exists', characters_one: 'character', characters_other: 'characters', hitCount: 'Retrieval count', diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts index 3e251d1bf1..e5d3f5ff3e 100644 --- a/web/i18n/en-US/dataset.ts +++ b/web/i18n/en-US/dataset.ts @@ -183,6 +183,7 @@ const translation = { checkName: { empty: 'Metadata name cannot be empty', invalid: 'Metadata name can only contain lowercase letters, numbers, and underscores and must start with a lowercase letter', + tooLong: 'Metadata name cannot exceed {{max}} characters', }, batchEditMetadata: { editMetadata: 'Edit Metadata', diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 1eaa0cb0a0..cf9df89a6b 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -85,8 +85,8 @@ const translation = { settings: 'USER SETTINGS', params: 'REASONING CONFIG', paramsTip1: 'Controls LLM inference parameters.', - paramsTip2: 'When \'Automatic\' is off, the default value is used.', - auto: 'Automatic', + paramsTip2: 'When \'Auto\' is off, the default value is used.', + auto: 'Auto', empty: 'Click the \'+\' button to add tools. You can add multiple tools.', uninstalledTitle: 'Tool not installed', uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.', @@ -94,6 +94,7 @@ const translation = { unsupportedTitle: 'Unsupported Action', unsupportedContent: 'The installed plugin version does not provide this action.', unsupportedContent2: 'Click to switch version.', + unsupportedMCPTool: 'Currently selected agent strategy plugin version does not support MCP tools.', }, configureApp: 'Configure App', configureModel: 'Configure model', @@ -154,6 +155,7 @@ const translation = { next: 'Next', pluginLoadError: 'Plugin load error', pluginLoadErrorDesc: 'This plugin will not be installed', + installWarning: 'This plugin is not allowed to be installed.', }, installFromGitHub: { installPlugin: 'Install plugin from GitHub', @@ -210,7 +212,7 @@ const translation = { clearAll: 'Clear all', }, requestAPlugin: 'Request a plugin', - submitPlugin: 'Submit plugin', + publishPlugins: 'Publish plugins', difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}', } diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 433e98720a..4e1ce1308a 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'add', added: 'added', manageInTools: 'Manage in Tools', - emptyTitle: 'No workflow tool available', - emptyTip: 'Go to "Workflow -> Publish as Tool"', - emptyTitleCustom: 'No custom tool available', - emptyTipCustom: 'Create a custom tool', + custom: { + title: 'No custom tool available', + tip: 'Create a custom tool', + }, + workflow: { + title: 'No workflow tool available', + tip: 'Publish workflows as tools in Studio', + }, + mcp: { + title: 'No MCP tool available', + tip: 'Add an MCP server', + }, + agent: { + title: 'No agent strategy available', + }, }, createTool: { title: 'Create Custom Tool', @@ -69,11 +80,15 @@ const translation = { title: 'Authorization method', type: 'Authorization type', keyTooltip: 'Http Header Key, You can leave it with "Authorization" if you have no idea what it is or set it to a custom value', + queryParam: 'Query Parameter', + queryParamTooltip: 'The name of the API key query parameter to pass, e.g. "key" in "https://example.com/test?key=API_KEY".', types: { none: 'None', - api_key: 'API Key', + api_key_header: 'Header', + api_key_query: 'Query Param', apiKeyPlaceholder: 'HTTP header name for API Key', apiValuePlaceholder: 'Enter API Key', + queryParamPlaceholder: 'Query parameter name for API Key', }, key: 'Key', value: 'Value', @@ -152,6 +167,71 @@ const translation = { toolNameUsageTip: 'Tool call name for agent reasoning and prompting', copyToolName: 'Copy Name', noTools: 'No tools found', + mcp: { + create: { + cardTitle: 'Add MCP Server (HTTP)', + cardLink: 'Learn more about MCP server integration', + }, + noConfigured: 'Unconfigured', + updateTime: 'Updated', + toolsCount: '{{count}} tools', + noTools: 'No tools available', + modal: { + title: 'Add MCP Server (HTTP)', + editTitle: 'Edit MCP Server (HTTP)', + name: 'Name & Icon', + namePlaceholder: 'Name your MCP server', + serverUrl: 'Server URL', + serverUrlPlaceholder: 'URL to server endpoint', + serverUrlWarning: 'Updating the server address may disrupt applications that depend on this server', + serverIdentifier: 'Server Identifier', + serverIdentifierTip: 'Unique identifier for the MCP server within the workspace. Lowercase letters, numbers, underscores, and hyphens only. Up to 24 characters.', + serverIdentifierPlaceholder: 'Unique identifier, e.g., my-mcp-server', + serverIdentifierWarning: 'The server won’t be recognized by existing apps after an ID change', + cancel: 'Cancel', + save: 'Save', + confirm: 'Add & Authorize', + }, + delete: 'Remove MCP Server', + deleteConfirmTitle: 'Would you like to remove {{mcp}}?', + operation: { + edit: 'Edit', + remove: 'Remove', + }, + authorize: 'Authorize', + authorizing: 'Authorizing...', + authorizingRequired: 'Authorization is required', + authorizeTip: 'After authorization, tools will be displayed here.', + update: 'Update', + updating: 'Updating', + gettingTools: 'Getting Tools...', + updateTools: 'Updating Tools...', + toolsEmpty: 'Tools not loaded', + getTools: 'Get tools', + toolUpdateConfirmTitle: 'Update Tool List', + toolUpdateConfirmContent: 'Updating the tool list may affect existing apps. Do you wish to proceed?', + toolsNum: '{{count}} tools included', + onlyTool: '1 tool included', + identifier: 'Server Identifier (Click to Copy)', + server: { + title: 'MCP Server', + url: 'Server URL', + reGen: 'Do you want to regenerator server URL?', + addDescription: 'Add description', + edit: 'Edit description', + modal: { + addTitle: 'Add description to enable MCP server', + editTitle: 'Edit description', + description: 'Description', + descriptionPlaceholder: 'Explain what this tool does and how it should be used by the LLM', + parameters: 'Parameters', + parametersTip: 'Add descriptions for each parameter to help the LLM understand their purpose and constraints.', + parametersPlaceholder: 'Parameter purpose and constraints', + confirm: 'Enable MCP Server', + }, + publishTip: 'App not published. Please publish the app first.', + }, + }, } export default translation diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 57cb42a0b1..763739ba32 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -46,8 +46,8 @@ const translation = { setVarValuePlaceholder: 'Set variable', needConnectTip: 'This step is not connected to anything', maxTreeDepth: 'Maximum limit of {{depth}} nodes per branch', - needEndNode: 'The End block must be added', - needAnswerNode: 'The Answer block must be added', + needEndNode: 'The End node must be added', + needAnswerNode: 'The Answer node must be added', workflowProcess: 'Workflow Process', notRunning: 'Not running yet', previewPlaceholder: 'Enter content in the box below to start debugging the Chatbot', @@ -66,7 +66,7 @@ const translation = { learnMore: 'Learn More', copy: 'Copy', duplicate: 'Duplicate', - addBlock: 'Add Block', + addBlock: 'Add Node', pasteHere: 'Paste Here', pointerMode: 'Pointer Mode', handMode: 'Hand Mode', @@ -113,6 +113,7 @@ const translation = { addFailureBranch: 'Add Fail Branch', loadMore: 'Load More', noHistory: 'No History', + tagBound: 'Number of apps using this tag', }, env: { envPanelTitle: 'Environment Variables', @@ -127,6 +128,8 @@ const translation = { value: 'Value', valuePlaceholder: 'env value', secretTip: 'Used to define sensitive information or data, with DSL settings configured for leak prevention.', + description: 'Description', + descriptionPlaceholder: 'Describe the variable', }, export: { title: 'Export Secret environment variables?', @@ -174,19 +177,19 @@ const translation = { stepForward_other: '{{count}} steps forward', sessionStart: 'Session Start', currentState: 'Current State', - nodeTitleChange: 'Block title changed', - nodeDescriptionChange: 'Block description changed', - nodeDragStop: 'Block moved', - nodeChange: 'Block changed', - nodeConnect: 'Block connected', - nodePaste: 'Block pasted', - nodeDelete: 'Block deleted', - nodeAdd: 'Block added', - nodeResize: 'Block resized', + nodeTitleChange: 'Node title changed', + nodeDescriptionChange: 'Node description changed', + nodeDragStop: 'Node moved', + nodeChange: 'Node changed', + nodeConnect: 'Node connected', + nodePaste: 'Node pasted', + nodeDelete: 'Node deleted', + nodeAdd: 'Node added', + nodeResize: 'Node resized', noteAdd: 'Note added', noteChange: 'Note changed', noteDelete: 'Note deleted', - edgeDelete: 'Block disconnected', + edgeDelete: 'Node disconnected', }, errorMsg: { fieldRequired: '{{field}} is required', @@ -215,8 +218,8 @@ const translation = { loop: 'Loop', }, tabs: { - 'searchBlock': 'Search block', - 'blocks': 'Blocks', + 'searchBlock': 'Search node', + 'blocks': 'Nodes', 'searchTool': 'Search tool', 'tools': 'Tools', 'allTool': 'All', @@ -229,6 +232,8 @@ const translation = { 'utilities': 'Utilities', 'noResult': 'No match found', 'agent': 'Agent Strategy', + 'allAdded': 'All added', + 'addAll': 'Add all', }, blocks: { 'start': 'Start', @@ -292,21 +297,23 @@ const translation = { }, panel: { userInputField: 'User Input Field', - changeBlock: 'Change Block', + changeBlock: 'Change Node', helpLink: 'Help Link', about: 'About', createdBy: 'Created By ', nextStep: 'Next Step', - addNextStep: 'Add the next block in this workflow', - selectNextStep: 'Select Next Block', + addNextStep: 'Add the next step in this workflow', + selectNextStep: 'Select Next Step', runThisStep: 'Run this step', moveToThisNode: 'Move to this node', checklist: 'Checklist', checklistTip: 'Make sure all issues are resolved before publishing', checklistResolved: 'All issues are resolved', - organizeBlocks: 'Organize blocks', + organizeBlocks: 'Organize nodes', change: 'Change', optional: '(optional)', + maximize: 'Maximize Canvas', + minimize: 'Exit Full Screen', }, nodes: { common: { @@ -364,6 +371,10 @@ const translation = { ms: 'ms', retries: '{{num}} Retries', }, + typeSwitch: { + input: 'Input value', + variable: 'Use variable', + }, }, start: { required: 'required', @@ -540,6 +551,10 @@ const translation = { title: 'Import from cURL', placeholder: 'Paste cURL string here', }, + verifySSL: { + title: 'Verify SSL Certificate', + warningTooltip: 'Disabling SSL verification is not recommended for production environments. This should only be used in development or testing, as it makes the connection vulnerable to security threats like man-in-the-middle attacks.', + }, }, code: { inputVars: 'Input Variables', @@ -547,6 +562,7 @@ const translation = { advancedDependencies: 'Advanced Dependencies', advancedDependenciesTip: 'Add some preloaded dependencies that take more time to consume or are not default built-in here', searchDependencies: 'Search Dependencies', + syncFunctionSignature: 'Sync function signature to code', }, templateTransform: { inputVars: 'Input Variables', @@ -653,6 +669,9 @@ const translation = { tool: { authorize: 'Authorize', inputVars: 'Input Variables', + settings: 'Settings', + insertPlaceholder1: 'Type or press', + insertPlaceholder2: 'insert variable', outputVars: { text: 'tool generated content', files: { @@ -670,6 +689,7 @@ const translation = { inputVars: 'Input Variables', outputVars: { className: 'Class Name', + usage: 'Model Usage Information', }, class: 'Class', classNamePlaceholder: 'Write your class name', @@ -683,6 +703,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Input Variable', + outputVars: { + isSuccess: 'Is Success.On success the value is 1, on failure the value is 0.', + errorReason: 'Error Reason', + usage: 'Model Usage Information', + }, extractParameters: 'Extract Parameters', importFromTool: 'Import from tools', addExtractParameter: 'Add Extract Parameter', @@ -702,8 +727,6 @@ const translation = { advancedSetting: 'Advanced Setting', reasoningMode: 'Reasoning Mode', reasoningModeTip: 'You can choose the appropriate reasoning mode based on the model\'s ability to respond to instructions for function calling or prompts.', - isSuccess: 'Is Success.On success the value is 1, on failure the value is 0.', - errorReason: 'Error Reason', }, iteration: { deleteTitle: 'Delete Iteration Node?', @@ -876,6 +899,8 @@ const translation = { install: 'Install', cancel: 'Cancel', }, + clickToViewParameterSchema: 'Click to view parameter schema', + parameterSchema: 'Parameter Schema', }, }, tracing: { @@ -913,6 +938,35 @@ const translation = { updateFailure: 'Failed to update version', }, }, + debug: { + settingsTab: 'Settings', + lastRunTab: 'Last Run', + noData: { + description: 'The results of the last run will be displayed here', + runThisNode: 'Run this node', + }, + variableInspect: { + title: 'Variable Inspect', + emptyTip: 'After stepping through a node on the canvas or running a node step by step, you can view the current value of the node variable in Variable Inspect', + emptyLink: 'Learn more', + clearAll: 'Reset all', + clearNode: 'Clear cached variable', + resetConversationVar: 'Reset conversation variable to default value', + view: 'View log', + edited: 'Edited', + reset: 'Reset to last run value', + trigger: { + normal: 'Variable Inspect', + running: 'Caching running status', + stop: 'Stop run', + cached: 'View cached variables', + clear: 'Clear', + }, + envNode: 'Environment', + chatNode: 'Conversation', + systemNode: 'System', + }, + }, } export default translation diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index 8c986bf669..afdea66338 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -329,6 +329,7 @@ const translation = { writeOpener: 'Escribir apertura', placeholder: 'Escribe tu mensaje de apertura aquí, puedes usar variables, intenta escribir {{variable}}.', openingQuestion: 'Preguntas de Apertura', + openingQuestionPlaceholder: 'Puede usar variables, intente escribir {{variable}}.', noDataPlaceHolder: 'Iniciar la conversación con el usuario puede ayudar a la IA a establecer una conexión más cercana con ellos en aplicaciones de conversación.', varTip: 'Puedes usar variables, intenta escribir {{variable}}', tooShort: 'Se requieren al menos 20 palabras en la indicación inicial para generar una apertura de conversación.', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index c1147720e7..4c9497e16d 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -93,6 +93,7 @@ const translation = { foundResult: '{{conteo}} Resultado', chatbotUserDescription: 'Cree rápidamente un chatbot basado en LLM con una configuración sencilla. Puedes cambiar a Chatflow más tarde.', completionUserDescription: 'Cree rápidamente un asistente de IA para tareas de generación de texto con una configuración sencilla.', + dropDSLToCreateApp: 'Suelta el archivo DSL aquí para crear la aplicación', }, editApp: 'Editar información', editAppTitle: 'Editar información de la app', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Configurar proveedor para habilitar el rastreo', moreProvider: 'Más proveedores', }, + arize: { + title: 'Arize', + description: 'Observabilidad de LLM de nivel empresarial, evaluación en línea y fuera de línea, monitoreo y experimentación—impulsada por OpenTelemetry. Diseñada específicamente para aplicaciones impulsadas por LLM y agentes.', + }, + phoenix: { + title: 'Phoenix', + description: 'Plataforma de observabilidad, evaluación, ingeniería de prompts y experimentación de código abierto basada en OpenTelemetry para sus flujos de trabajo y agentes de LLM.', + }, langsmith: { title: 'LangSmith', description: 'Una plataforma de desarrollo todo en uno para cada paso del ciclo de vida de la aplicación impulsada por LLM.', @@ -163,6 +172,7 @@ const translation = { description: 'Weave es una plataforma de código abierto para evaluar, probar y monitorear aplicaciones de LLM.', title: 'Tejer', }, + aliyun: {}, }, answerIcon: { title: 'Usar el icono de la aplicación web para reemplazar 🤖', @@ -207,6 +217,7 @@ const translation = { modelNotSupportedTip: 'El modelo actual no admite esta función y se degrada automáticamente a inyección de comandos.', structuredTip: 'Las Salidas Estructuradas son una función que garantiza que el modelo siempre generará respuestas que se ajusten a su esquema JSON proporcionado.', modelNotSupported: 'Modelo no soportado', + structured: 'sistemático', }, accessItemsDescription: { anyone: 'Cualquiera puede acceder a la aplicación web.', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 82ed315f1c..0b2579482e 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -58,6 +58,8 @@ const translation = { downloadSuccess: 'Descarga completada.', downloadFailed: 'La descarga ha fallado. Por favor, inténtalo de nuevo más tarde.', format: 'Formato', + deSelectAll: 'Deseleccionar todo', + selectAll: 'Seleccionar todo', }, errorMsg: { fieldRequired: '{{field}} es requerido', @@ -471,7 +473,6 @@ const translation = { apiBasedExtension: { title: 'Las extensiones basadas en API proporcionan una gestión centralizada de API, simplificando la configuración para su fácil uso en las aplicaciones de Dify.', link: 'Aprende cómo desarrollar tu propia Extensión API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Agregar Extensión API', selector: { title: 'Extensión API', diff --git a/web/i18n/es-ES/dataset-creation.ts b/web/i18n/es-ES/dataset-creation.ts index 39846883d1..7bb62e1ea3 100644 --- a/web/i18n/es-ES/dataset-creation.ts +++ b/web/i18n/es-ES/dataset-creation.ts @@ -63,7 +63,6 @@ const translation = { run: 'Ejecutar', firecrawlTitle: 'Extraer contenido web con 🔥Firecrawl', firecrawlDoc: 'Documentación de Firecrawl', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', options: 'Opciones', crawlSubPage: 'Rastrear subpáginas', limit: 'Límite', @@ -92,7 +91,6 @@ const translation = { configureFirecrawl: 'Configurar Firecrawl', watercrawlDoc: 'Documentos de Watercrawl', configureJinaReader: 'Configurar Jina Reader', - watercrawlDocLink: 'https://docs.dify.ai/es/guías/base-de-conocimientos/crear-conocimientos-y-subir-documentos/importar-datos-de-contenido/sincronizar-desde-el-sitio-web', configureWatercrawl: 'Configurar Watercrawl', waterCrawlNotConfiguredDescription: 'Configura Watercrawl con la clave de API para usarlo.', }, diff --git a/web/i18n/es-ES/dataset-documents.ts b/web/i18n/es-ES/dataset-documents.ts index cd5bb36197..ee6c5bcf74 100644 --- a/web/i18n/es-ES/dataset-documents.ts +++ b/web/i18n/es-ES/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'Eliminar', enableWarning: 'El archivo archivado no puede habilitarse', sync: 'Sincronizar', + resume: 'Reanudar', + pause: 'Pausa', }, index: { enable: 'Habilitar', @@ -389,6 +391,8 @@ const translation = { characters_one: 'carácter', regenerationSuccessMessage: 'Puede cerrar esta ventana.', regenerationConfirmTitle: '¿Desea regenerar fragmentos secundarios?', + keywordEmpty: 'La palabra clave no puede estar vacía', + keywordDuplicate: 'La palabra clave ya existe', }, } diff --git a/web/i18n/es-ES/dataset.ts b/web/i18n/es-ES/dataset.ts index b759f26ec8..16745b56d7 100644 --- a/web/i18n/es-ES/dataset.ts +++ b/web/i18n/es-ES/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'El nombre de metadatos no puede estar vacío', invalid: 'El nombre de los metadatos solo puede contener letras minúsculas, números y guiones bajos, y debe comenzar con una letra minúscula.', + tooLong: 'El nombre de los metadatos no puede exceder {{max}} caracteres.', }, batchEditMetadata: { multipleValue: 'Valor Múltiple', @@ -202,6 +203,7 @@ const translation = { builtInDescription: 'Los metadatos integrados se extraen y generan automáticamente. Deben estar habilitados antes de su uso y no se pueden editar.', name: 'Nombre', description: 'Puedes gestionar todos los metadatos en este conocimiento aquí. Las modificaciones se sincronizarán en todos los documentos.', + disabled: 'desactivar', }, documentMetadata: { technicalParameters: 'Parámetros técnicos', diff --git a/web/i18n/es-ES/plugin.ts b/web/i18n/es-ES/plugin.ts index 4c1f148235..84e317add6 100644 --- a/web/i18n/es-ES/plugin.ts +++ b/web/i18n/es-ES/plugin.ts @@ -51,11 +51,11 @@ const translation = { unsupportedContent2: 'Haga clic para cambiar de versión.', descriptionPlaceholder: 'Breve descripción del propósito de la herramienta, por ejemplo, obtener la temperatura para una ubicación específica.', empty: 'Haga clic en el botón \'+\' para agregar herramientas. Puede agregar varias herramientas.', - paramsTip2: 'Cuando \'Automático\' está desactivado, se utiliza el valor predeterminado.', + paramsTip2: 'Cuando \'Auto\' está desactivado, se utiliza el valor predeterminado.', uninstalledTitle: 'Herramienta no instalada', descriptionLabel: 'Descripción de la herramienta', unsupportedContent: 'La versión del plugin instalado no proporciona esta acción.', - auto: 'Automático', + auto: 'Auto', title: 'Agregar herramienta', placeholder: 'Seleccione una herramienta...', uninstalledContent: 'Este plugin se instala desde el repositorio local/GitHub. Úselo después de la instalación.', @@ -137,6 +137,7 @@ const translation = { dropPluginToInstall: 'Suelte el paquete del complemento aquí para instalarlo', readyToInstallPackage: 'A punto de instalar el siguiente plugin', installedSuccessfully: 'Instalación exitosa', + installWarning: 'Este plugin no está permitido para instalar.', }, installFromGitHub: { uploadFailed: 'Error de carga', @@ -175,7 +176,7 @@ const translation = { empower: 'Potencie su desarrollo de IA', moreFrom: 'Más de Marketplace', viewMore: 'Ver más', - sortBy: 'Ciudad negra', + sortBy: 'Ordenar por', noPluginFound: 'No se ha encontrado ningún plugin', pluginsResult: '{{num}} resultados', discover: 'Descubrir', @@ -195,7 +196,6 @@ const translation = { fromMarketplace: 'De Marketplace', endpointsEnabled: '{{num}} conjuntos de puntos finales habilitados', from: 'De', - submitPlugin: 'Enviar plugin', installAction: 'Instalar', install: '{{num}} instalaciones', allCategories: 'Todas las categorías', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'La versión actual de Dify no es compatible con este plugin, por favor actualiza a la versión mínima requerida: {{minimalDifyVersion}}', requestAPlugin: 'Solicitar un plugin', + publishPlugins: 'Publicar plugins', } export default translation diff --git a/web/i18n/es-ES/tools.ts b/web/i18n/es-ES/tools.ts index fd37eef5b1..b503f9c41b 100644 --- a/web/i18n/es-ES/tools.ts +++ b/web/i18n/es-ES/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'agregar', added: 'agregada', manageInTools: 'Administrar en Herramientas', - emptyTitle: 'No hay herramientas de flujo de trabajo disponibles', - emptyTip: 'Ir a "Flujo de Trabajo -> Publicar como Herramienta"', - emptyTitleCustom: 'No hay herramienta personalizada disponible', - emptyTipCustom: 'Crear una herramienta personalizada', + custom: { + title: 'No hay herramienta personalizada disponible', + tip: 'Crear una herramienta personalizada', + }, + workflow: { + title: 'No hay herramienta de flujo de trabajo disponible', + tip: 'Publicar flujos de trabajo como herramientas en el Estudio', + }, + mcp: { + title: 'No hay herramienta MCP disponible', + tip: 'Añadir un servidor MCP', + }, + agent: { + title: 'No hay estrategia de agente disponible', + }, }, createTool: { title: 'Crear Herramienta Personalizada', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Nombre de llamada de la herramienta para razonamiento y promoción de agentes', copyToolName: 'Nombre de la copia', noTools: 'No se han encontrado herramientas', + mcp: { + create: { + cardTitle: 'Añadir servidor MCP (HTTP)', + cardLink: 'Más información sobre integración de servidores MCP', + }, + noConfigured: 'Servidor no configurado', + updateTime: 'Actualizado', + toolsCount: '{{count}} herramientas', + noTools: 'No hay herramientas disponibles', + modal: { + title: 'Añadir servidor MCP (HTTP)', + editTitle: 'Editar servidor MCP (HTTP)', + name: 'Nombre e Icono', + namePlaceholder: 'Nombre de su servidor MCP', + serverUrl: 'URL del servidor', + serverUrlPlaceholder: 'URL del endpoint del servidor', + serverUrlWarning: 'Actualizar la dirección del servidor puede interrumpir aplicaciones que dependan de él', + serverIdentifier: 'Identificador del servidor', + serverIdentifierTip: 'Identificador único del servidor MCP en el espacio de trabajo. Solo letras minúsculas, números, guiones bajos y guiones. Máximo 24 caracteres.', + serverIdentifierPlaceholder: 'Identificador único, ej. mi-servidor-mcp', + serverIdentifierWarning: 'El servidor no será reconocido por aplicaciones existentes tras cambiar la ID', + cancel: 'Cancelar', + save: 'Guardar', + confirm: 'Añadir y Autorizar', + }, + delete: 'Eliminar servidor MCP', + deleteConfirmTitle: '¿Eliminar {{mcp}}?', + operation: { + edit: 'Editar', + remove: 'Eliminar', + }, + authorize: 'Autorizar', + authorizing: 'Autorizando...', + authorizingRequired: 'Se requiere autorización', + authorizeTip: 'Tras la autorización, las herramientas se mostrarán aquí.', + update: 'Actualizar', + updating: 'Actualizando', + gettingTools: 'Obteniendo herramientas...', + updateTools: 'Actualizando herramientas...', + toolsEmpty: 'Herramientas no cargadas', + getTools: 'Obtener herramientas', + toolUpdateConfirmTitle: 'Actualizar lista de herramientas', + toolUpdateConfirmContent: 'Actualizar la lista puede afectar a aplicaciones existentes. ¿Continuar?', + toolsNum: '{{count}} herramientas incluidas', + onlyTool: '1 herramienta incluida', + identifier: 'Identificador del servidor (Haz clic para copiar)', + server: { + title: 'Servidor MCP', + url: 'URL del servidor', + reGen: '¿Regenerar URL del servidor?', + addDescription: 'Añadir descripción', + edit: 'Editar descripción', + modal: { + addTitle: 'Añade descripción para habilitar el servidor MCP', + editTitle: 'Editar descripción', + description: 'Descripción', + descriptionPlaceholder: 'Explica qué hace esta herramienta y cómo debe usarla el LLM', + parameters: 'Parámetros', + parametersTip: 'Añade descripciones de cada parámetro para ayudar al LLM a entender su propósito y restricciones.', + parametersPlaceholder: 'Propósito y restricciones del parámetro', + confirm: 'Habilitar servidor MCP', + }, + publishTip: 'App no publicada. Publícala primero.', + }, + }, } export default translation diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 7881728c89..44516317e8 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Establecer variable', needConnectTip: 'Este paso no está conectado a nada', maxTreeDepth: 'Límite máximo de {{depth}} nodos por rama', - needEndNode: 'Debe agregarse el bloque de Fin', - needAnswerNode: 'Debe agregarse el bloque de Respuesta', workflowProcess: 'Proceso de flujo de trabajo', notRunning: 'Aún no se está ejecutando', previewPlaceholder: 'Ingrese contenido en el cuadro de abajo para comenzar a depurar el Chatbot', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Más información', copy: 'Copiar', duplicate: 'Duplicar', - addBlock: 'Agregar bloque', pasteHere: 'Pegar aquí', pointerMode: 'Modo puntero', handMode: 'Modo mano', @@ -115,6 +112,9 @@ const translation = { publishUpdate: 'Publicar actualización', noExist: 'No existe tal variable', exportImage: 'Exportar imagen', + needAnswerNode: 'Se debe agregar el nodo de respuesta', + needEndNode: 'Se debe agregar el nodo Final', + addBlock: 'Agregar nodo', }, env: { envPanelTitle: 'Variables de Entorno', @@ -129,6 +129,8 @@ const translation = { value: 'Valor', valuePlaceholder: 'valor de env', secretTip: 'Se utiliza para definir información o datos sensibles, con configuraciones DSL configuradas para prevenir fugas.', + description: 'Descripción', + descriptionPlaceholder: 'Describa la variable', }, export: { title: '¿Exportar variables de entorno secretas?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} pasos hacia adelante', sessionStart: 'Inicio de sesión', currentState: 'Estado actual', - nodeTitleChange: 'Se cambió el título del bloque', - nodeDescriptionChange: 'Se cambió la descripción del bloque', - nodeDragStop: 'Bloque movido', - nodeChange: 'Bloque cambiado', - nodeConnect: 'Bloque conectado', - nodePaste: 'Bloque pegado', - nodeDelete: 'Bloque eliminado', - nodeAdd: 'Bloque agregado', - nodeResize: 'Bloque redimensionado', noteAdd: 'Nota agregada', noteChange: 'Nota cambiada', noteDelete: 'Nota eliminada', - edgeDelete: 'Bloque desconectado', + nodeTitleChange: 'Título del nodo cambiado', + nodeAdd: 'Nodo añadido', + nodePaste: 'Nodo pegado', + nodeDragStop: 'Nodo movido', + nodeConnect: 'Nodo conectado', + edgeDelete: 'Nodo desconectado', + nodeDelete: 'Nodo eliminado', + nodeChange: 'Nodo cambiado', + nodeDescriptionChange: 'Descripción del nodo cambiada', + nodeResize: 'Nodo redimensionado', }, errorMsg: { fieldRequired: 'Se requiere {{field}}', @@ -217,8 +219,6 @@ const translation = { loop: 'Bucle', }, tabs: { - 'searchBlock': 'Buscar bloque', - 'blocks': 'Bloques', 'tools': 'Herramientas', 'allTool': 'Todos', 'builtInTool': 'Incorporadas', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Herramienta de búsqueda', 'agent': 'Estrategia del agente', 'plugin': 'Plugin', + 'searchBlock': 'Buscar nodo', + 'blocks': 'Nodos', }, blocks: { 'start': 'Inicio', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Campo de entrada del usuario', - changeBlock: 'Cambiar bloque', helpLink: 'Enlace de ayuda', about: 'Acerca de', createdBy: 'Creado por ', nextStep: 'Siguiente paso', - addNextStep: 'Agregar el siguiente bloque en este flujo de trabajo', - selectNextStep: 'Seleccionar siguiente bloque', runThisStep: 'Ejecutar este paso', checklist: 'Lista de verificación', checklistTip: 'Asegúrate de resolver todos los problemas antes de publicar', checklistResolved: 'Se resolvieron todos los problemas', - organizeBlocks: 'Organizar bloques', change: 'Cambiar', optional: '(opcional)', moveToThisNode: 'Mueve a este nodo', + organizeBlocks: 'Organizar nodos', + addNextStep: 'Agrega el siguiente paso en este flujo de trabajo', + changeBlock: 'Cambiar Nodo', + selectNextStep: 'Seleccionar siguiente paso', + maximize: 'Maximizar Canvas', + minimize: 'Salir de pantalla completa', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retries: '{{num}} Reintentos', retry: 'Reintentar', }, + typeSwitch: {}, }, start: { required: 'requerido', @@ -533,6 +538,10 @@ const translation = { title: 'Importar desde cURL', placeholder: 'Pegar la cadena cURL aquí', }, + verifySSL: { + title: 'Verificar el certificado SSL', + warningTooltip: 'Deshabilitar la verificación SSL no se recomienda para entornos de producción. Esto solo debe utilizarse en desarrollo o pruebas, ya que hace que la conexión sea vulnerable a amenazas de seguridad como ataques de intermediario.', + }, }, code: { inputVars: 'Variables de entrada', @@ -665,6 +674,7 @@ const translation = { inputVars: 'Variables de entrada', outputVars: { className: 'Nombre de la clase', + usage: 'Información de uso del modelo', }, class: 'Clase', classNamePlaceholder: 'Escribe el nombre de tu clase', @@ -678,6 +688,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Variable de entrada', + outputVars: { + isSuccess: 'Es éxito. En caso de éxito el valor es 1, en caso de fallo el valor es 0.', + errorReason: 'Motivo del error', + usage: 'Información de uso del modelo', + }, extractParameters: 'Extraer parámetros', importFromTool: 'Importar desde herramientas', addExtractParameter: 'Agregar parámetro de extracción', @@ -697,8 +712,6 @@ const translation = { advancedSetting: 'Configuración avanzada', reasoningMode: 'Modo de razonamiento', reasoningModeTip: 'Puede elegir el modo de razonamiento apropiado basado en la capacidad del modelo para responder a instrucciones para llamadas de funciones o indicaciones.', - isSuccess: 'Es éxito. En caso de éxito el valor es 1, en caso de fallo el valor es 0.', - errorReason: 'Motivo del error', }, iteration: { deleteTitle: '¿Eliminar nodo de iteración?', @@ -915,6 +928,33 @@ const translation = { currentDraft: 'Borrador Actual', editVersionInfo: 'Editar información de la versión', }, + debug: { + noData: { + runThisNode: 'Ejecuta este nodo', + description: 'Los resultados de la última ejecución se mostrarán aquí', + }, + variableInspect: { + trigger: { + running: 'Estado de ejecución de la caché', + stop: 'Detén la carrera', + normal: 'Inspeccionar Variable', + cached: 'Ver variables en caché', + }, + envNode: 'Medio ambiente', + chatNode: 'Conversación', + systemNode: 'Sistema', + view: 'Ver registro', + clearAll: 'Restablecer todo', + emptyLink: 'Aprender más', + title: 'Inspeccionar Variable', + reset: 'Restablecer al último valor ejecutado', + resetConversationVar: 'Restablecer la variable de conversación al valor predeterminado', + clearNode: 'Limpiar variable en caché', + emptyTip: 'Después de recorrer un nodo en el lienzo o ejecutar un nodo paso a paso, puedes ver el valor actual de la variable del nodo en Inspección de Variables.', + }, + lastRunTab: 'Última ejecución', + settingsTab: 'Ajustes', + }, } export default translation diff --git a/web/i18n/fa-IR/app-debug.ts b/web/i18n/fa-IR/app-debug.ts index 5cf9c15efe..75085ef30e 100644 --- a/web/i18n/fa-IR/app-debug.ts +++ b/web/i18n/fa-IR/app-debug.ts @@ -364,6 +364,7 @@ const translation = { writeOpener: 'نوشتن آغازگر', placeholder: 'پیام آغازگر خود را اینجا بنویسید، می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', openingQuestion: 'سوالات آغازین', + openingQuestionPlaceholder: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', noDataPlaceHolder: 'شروع مکالمه با کاربر می‌تواند به AI کمک کند تا ارتباط نزدیک‌تری با آنها برقرار کند.', varTip: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید', tooShort: 'حداقل 20 کلمه از پرسش اولیه برای تولید نظرات آغازین مکالمه مورد نیاز است.', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 13473d21f5..bf2fa00c11 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -97,6 +97,7 @@ const translation = { completionUserDescription: 'به سرعت یک دستیار هوش مصنوعی برای وظایف تولید متن با پیکربندی ساده بسازید.', advancedShortDescription: 'گردش‌کار پیشرفته برای گفتگوهای چند مرحله‌ای', agentUserDescription: 'یک عامل هوشمند که قادر به استدلال تکراری و استفاده از ابزار مستقل برای دستیابی به اهداف وظیفه است.', + dropDSLToCreateApp: 'فایل DSL را اینجا رها کنید تا برنامه ساخته شود', }, editApp: 'ویرایش اطلاعات', editAppTitle: 'ویرایش اطلاعات برنامه', @@ -139,6 +140,14 @@ const translation = { notConfigured: 'برای فعال‌سازی ردیابی ارائه‌دهنده را پیکربندی کنید', moreProvider: 'ارائه‌دهندگان بیشتر', }, + arize: { + title: 'Arize', + description: 'قابلیت مشاهده LLM در سطح سازمانی، ارزیابی آنلاین و آفلاین، نظارت و آزمایش — با پشتیبانی از OpenTelemetry. طراحی‌شده مخصوص برنامه‌های مبتنی بر LLM و عامل‌ها.', + }, + phoenix: { + title: 'Phoenix', + description: 'پلتفرم متن‌باز و مبتنی بر OpenTelemetry برای مشاهده‌پذیری، ارزیابی، مهندسی پرامپت و آزمایش برای جریان‌های کاری و عامل‌های LLM شما.', + }, langsmith: { title: 'LangSmith', description: 'یک پلتفرم همه‌کاره برای هر مرحله از چرخه عمر برنامه‌های مبتنی بر LLM.', @@ -167,6 +176,7 @@ const translation = { title: 'بافندگی', description: 'ویو یک پلتفرم متن باز برای ارزیابی، آزمایش و نظارت بر برنامه‌های LLM است.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', diff --git a/web/i18n/fa-IR/common.ts b/web/i18n/fa-IR/common.ts index 7cd9a89684..7adbad8ca4 100644 --- a/web/i18n/fa-IR/common.ts +++ b/web/i18n/fa-IR/common.ts @@ -58,6 +58,8 @@ const translation = { more: 'بیشتر', format: 'قالب', downloadSuccess: 'دانلود کامل شد.', + selectAll: 'انتخاب همه', + deSelectAll: 'همه را انتخاب نکنید', }, errorMsg: { fieldRequired: '{{field}} الزامی است', @@ -471,7 +473,6 @@ const translation = { apiBasedExtension: { title: 'افزونه‌های مبتنی بر API مدیریت متمرکز API را فراهم می‌کنند و پیکربندی را برای استفاده آسان در برنامه‌های Dify ساده می‌کنند.', link: 'نحوه توسعه افزونه API خود را بیاموزید.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'افزودن افزونه API', selector: { title: 'افزونه API', diff --git a/web/i18n/fa-IR/dataset-creation.ts b/web/i18n/fa-IR/dataset-creation.ts index 4d938bbafe..bf3a03e0c2 100644 --- a/web/i18n/fa-IR/dataset-creation.ts +++ b/web/i18n/fa-IR/dataset-creation.ts @@ -63,7 +63,6 @@ const translation = { run: 'اجرا', firecrawlTitle: 'استخراج محتوای وب با fireFirecrawl', firecrawlDoc: 'مستندات Firecrawl', - firecrawlDocLink: '<a href="https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website">https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website</a>', options: 'گزینهها', crawlSubPage: 'خزش صفحات فرعی', limit: 'محدودیت', @@ -92,7 +91,6 @@ const translation = { waterCrawlNotConfiguredDescription: 'برای استفاده از Watercrawl، آن را با کلید API پیکربندی کنید.', waterCrawlNotConfigured: 'Watercrawl پیکربندی نشده است', configureJinaReader: 'پیکربندی خواننده جینا', - watercrawlDocLink: 'https://docs.dify.ai/fa/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', watercrawlTitle: 'محتوای وب را با واترکرال استخراج کنید', configureWatercrawl: 'تنظیم واترکراول', }, diff --git a/web/i18n/fa-IR/dataset-documents.ts b/web/i18n/fa-IR/dataset-documents.ts index 85e1e0a4aa..8da7ba8959 100644 --- a/web/i18n/fa-IR/dataset-documents.ts +++ b/web/i18n/fa-IR/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'حذف', enableWarning: 'فایل بایگانی شده نمی‌تواند فعال شود', sync: 'همگام‌سازی', + resume: 'ادامه', + pause: 'مکث', }, index: { enable: 'فعال کردن', @@ -388,6 +390,8 @@ const translation = { regeneratingMessage: 'این ممکن است یک لحظه طول بکشد، لطفا صبر کنید...', regenerationConfirmTitle: 'آیا می خواهید تکه های کودک را بازسازی کنید؟', regenerationSuccessMessage: 'می توانید این پنجره را ببندید.', + keywordEmpty: 'کلمه کلیدی نمی‌تواند خالی باشد', + keywordDuplicate: 'این کلیدواژه قبلاً وجود دارد', }, } diff --git a/web/i18n/fa-IR/dataset.ts b/web/i18n/fa-IR/dataset.ts index 8ab2fb9179..9b2069d778 100644 --- a/web/i18n/fa-IR/dataset.ts +++ b/web/i18n/fa-IR/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { invalid: 'نام متاداده فقط می‌تواند شامل حروف کوچک، اعداد و زیرخط‌ها باشد و باید با یک حرف کوچک آغاز شود.', empty: 'نام فراداده نمی‌تواند خالی باشد', + tooLong: 'نام متا داده نمی‌تواند بیشتر از {{max}} کاراکتر باشد', }, batchEditMetadata: { multipleValue: 'چندین ارزش', diff --git a/web/i18n/fa-IR/plugin.ts b/web/i18n/fa-IR/plugin.ts index 80433900c1..890d666f9f 100644 --- a/web/i18n/fa-IR/plugin.ts +++ b/web/i18n/fa-IR/plugin.ts @@ -137,6 +137,7 @@ const translation = { installPlugin: 'افزونه را نصب کنید', installComplete: 'نصب کامل شد', uploadFailed: 'آپلود انجام نشد', + installWarning: 'این افزونه اجازه نصب ندارد.', }, installFromGitHub: { installPlugin: 'افزونه را از GitHub نصب کنید', @@ -195,7 +196,6 @@ const translation = { searchTools: 'ابزارهای جستجو...', findMoreInMarketplace: 'اطلاعات بیشتر در Marketplace', searchInMarketplace: 'جستجو در Marketplace', - submitPlugin: 'ارسال افزونه', searchCategories: 'دسته بندی ها را جستجو کنید', fromMarketplace: 'از بازار', installPlugin: 'افزونه را نصب کنید', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'نسخه فعلی دیفی با این پلاگین سازگار نیست، لطفاً به نسخه حداقل مورد نیاز به‌روزرسانی کنید: {{minimalDifyVersion}}', requestAPlugin: 'درخواست یک افزونه', + publishPlugins: 'انتشار افزونه ها', } export default translation diff --git a/web/i18n/fa-IR/tools.ts b/web/i18n/fa-IR/tools.ts index fddfd2d826..942bde7932 100644 --- a/web/i18n/fa-IR/tools.ts +++ b/web/i18n/fa-IR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'افزودن', added: 'افزوده شد', manageInTools: 'مدیریت در ابزارها', - emptyTitle: 'هیچ ابزار جریان کاری در دسترس نیست', - emptyTip: 'به "جریان کاری -> انتشار به عنوان ابزار" بروید', - emptyTipCustom: 'ایجاد یک ابزار سفارشی', - emptyTitleCustom: 'هیچ ابزار سفارشی در دسترس نیست', + custom: { + title: 'هیچ ابزار سفارشی موجود نیست', + tip: 'یک ابزار سفارشی ایجاد کنید', + }, + workflow: { + title: 'هیچ ابزار جریان کاری موجود نیست', + tip: 'جریان‌های کاری را به عنوان ابزار در استودیو منتشر کنید', + }, + mcp: { + title: 'هیچ ابزار MCP موجود نیست', + tip: 'یک سرور MCP اضافه کنید', + }, + agent: { + title: 'هیچ استراتژی عاملی موجود نیست', + }, }, createTool: { title: 'ایجاد ابزار سفارشی', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'نام فراخوانی ابزار برای استدلال و پرامپت‌های عامل', copyToolName: 'کپی نام', noTools: 'هیچ ابزاری یافت نشد', + mcp: { + create: { + cardTitle: 'افزودن سرور MCP (HTTP)', + cardLink: 'اطلاعات بیشتر درباره یکپارچه‌سازی سرور MCP', + }, + noConfigured: 'سرور پیکربندی نشده', + updateTime: 'آخرین بروزرسانی', + toolsCount: '{count} ابزار', + noTools: 'ابزاری موجود نیست', + modal: { + title: 'افزودن سرور MCP (HTTP)', + editTitle: 'ویرایش سرور MCP (HTTP)', + name: 'نام و آیکون', + namePlaceholder: 'برای سرور MCP خود نام انتخاب کنید', + serverUrl: 'آدرس سرور', + serverUrlPlaceholder: 'URL نقطه پایانی سرور', + serverUrlWarning: 'به‌روزرسانی آدرس سرور ممکن است برنامه‌های وابسته به آن را مختل کند', + serverIdentifier: 'شناسه سرور', + serverIdentifierTip: 'شناسه منحصر به فرد برای سرور MCP در فضای کاری. فقط حروف کوچک، اعداد، زیرخط و خط تیره. حداکثر 24 کاراکتر.', + serverIdentifierPlaceholder: 'شناسه منحصر به فرد، مثال: my-mcp-server', + serverIdentifierWarning: 'پس از تغییر شناسه، سرور توسط برنامه‌های موجود شناسایی نخواهد شد', + cancel: 'لغو', + save: 'ذخیره', + confirm: 'افزودن و مجوزدهی', + }, + delete: 'حذف سرور MCP', + deleteConfirmTitle: 'آیا مایل به حذف {mcp} هستید؟', + operation: { + edit: 'ویرایش', + remove: 'حذف', + }, + authorize: 'مجوزدهی', + authorizing: 'در حال مجوزدهی...', + authorizingRequired: 'مجوز مورد نیاز است', + authorizeTip: 'پس از مجوزدهی، ابزارها در اینجا نمایش داده می‌شوند.', + update: 'به‌روزرسانی', + updating: 'در حال به‌روزرسانی...', + gettingTools: 'دریافت ابزارها...', + updateTools: 'به‌روزرسانی ابزارها...', + toolsEmpty: 'ابزارها بارگیری نشدند', + getTools: 'دریافت ابزارها', + toolUpdateConfirmTitle: 'به‌روزرسانی فهرست ابزارها', + toolUpdateConfirmContent: 'به‌روزرسانی فهرست ابزارها ممکن است بر برنامه‌های موجود تأثیر بگذارد. آیا ادامه می‌دهید؟', + toolsNum: '{count} ابزار موجود است', + onlyTool: '1 ابزار موجود است', + identifier: 'شناسه سرور (کلیک برای کپی)', + server: { + title: 'سرور MCP', + url: 'آدرس سرور', + reGen: 'تولید مجدد آدرس سرور؟', + addDescription: 'افزودن توضیحات', + edit: 'ویرایش توضیحات', + modal: { + addTitle: 'برای فعال‌سازی سرور MCP توضیحات اضافه کنید', + editTitle: 'ویرایش توضیحات', + description: 'توضیحات', + descriptionPlaceholder: 'عملکرد این ابزار و نحوه استفاده LLM از آن را توضیح دهید', + parameters: 'پارامترها', + parametersTip: 'برای کمک به LLM در درک هدف و محدودیت‌ها، برای هر پارامتر توضیحات اضافه کنید.', + parametersPlaceholder: 'هدف و محدودیت‌های پارامتر', + confirm: 'فعال‌سازی سرور MCP', + }, + publishTip: 'برنامه منتشر نشده است. لطفاً ابتدا برنامه را منتشر کنید.', + }, + }, } export default translation diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index e548dc8ecb..800dba06b8 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'تنظیم متغیر', needConnectTip: 'این مرحله به هیچ چیزی متصل نیست', maxTreeDepth: 'حداکثر عمق {{depth}} نود در هر شاخه', - needEndNode: 'بلوک پایان باید اضافه شود', - needAnswerNode: 'بلوک پاسخ باید اضافه شود', workflowProcess: 'فرآیند جریان کار', notRunning: 'هنوز در حال اجرا نیست', previewPlaceholder: 'محتوا را در کادر زیر وارد کنید تا اشکال‌زدایی چت‌بات را شروع کنید', @@ -58,7 +56,6 @@ const translation = { learnMore: 'اطلاعات بیشتر', copy: 'کپی', duplicate: 'تکرار', - addBlock: 'افزودن بلوک', pasteHere: 'چسباندن اینجا', pointerMode: 'حالت اشاره‌گر', handMode: 'حالت دست', @@ -115,6 +112,9 @@ const translation = { exportImage: 'تصویر را صادر کنید', versionHistory: 'تاریخچه نسخه', publishUpdate: 'به‌روزرسانی منتشر کنید', + needEndNode: 'باید گره پایان اضافه شود', + needAnswerNode: 'باید گره پاسخ اضافه شود', + addBlock: 'نود اضافه کنید', }, env: { envPanelTitle: 'متغیرهای محیطی', @@ -129,6 +129,8 @@ const translation = { value: 'مقدار', valuePlaceholder: 'مقدار متغیر', secretTip: 'برای تعریف اطلاعات حساس یا داده‌ها، با تنظیمات DSL برای جلوگیری از نشت پیکربندی شده است.', + description: 'توضیحات', + descriptionPlaceholder: 'متغیر را توصیف کنید', }, export: { title: 'آیا متغیرهای محیطی مخفی را صادر کنید؟', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} قدم به جلو', sessionStart: 'شروع جلسه', currentState: 'وضعیت کنونی', - nodeTitleChange: 'عنوان بلوک تغییر کرده است', - nodeDescriptionChange: 'توضیحات بلوک تغییر کرده است', - nodeDragStop: 'بلوک جابجا شده است', - nodeChange: 'بلوک تغییر کرده است', - nodeConnect: 'بلوک متصل شده است', - nodePaste: 'بلوک چسبانده شده است', - nodeDelete: 'بلوک حذف شده است', - nodeAdd: 'بلوک اضافه شده است', - nodeResize: 'اندازه بلوک تغییر کرده است', noteAdd: 'یادداشت اضافه شده است', noteChange: 'یادداشت تغییر کرده است', noteDelete: 'یادداشت حذف شده است', - edgeDelete: 'بلوک قطع شده است', + nodeDelete: 'نود حذف شد', + nodeAdd: 'نود اضافه شد', + nodeDragStop: 'گره منتقل شد', + edgeDelete: 'گره قطع شده است', + nodeResize: 'اندازه نود تغییر یافته است', + nodePaste: 'نود پیست شده است', + nodeTitleChange: 'عنوان نود تغییر کرد', + nodeConnect: 'گره متصل است', + nodeDescriptionChange: 'شرح نود تغییر کرد', + nodeChange: 'نود تغییر کرد', }, errorMsg: { fieldRequired: '{{field}} الزامی است', @@ -217,8 +219,6 @@ const translation = { loop: 'حلقه', }, tabs: { - 'searchBlock': 'جستجوی بلوک', - 'blocks': 'بلوک‌ها', 'tools': 'ابزارها', 'allTool': 'همه', 'builtInTool': 'درون‌ساخت', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'ابزار جستجو', 'plugin': 'افزونه', 'agent': 'استراتژی نمایندگی', + 'blocks': 'گره‌ها', + 'searchBlock': 'گره جستجو', }, blocks: { 'start': 'شروع', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'فیلد ورودی کاربر', - changeBlock: 'تغییر بلوک', helpLink: 'لینک کمک', about: 'درباره', createdBy: 'ساخته شده توسط', nextStep: 'مرحله بعدی', - addNextStep: 'افزودن بلوک بعدی به این جریان کار', - selectNextStep: 'انتخاب بلوک بعدی', runThisStep: 'اجرا کردن این مرحله', checklist: 'چک‌لیست', checklistTip: 'اطمینان حاصل کنید که همه مسائل قبل از انتشار حل شده‌اند', checklistResolved: 'تمام مسائل حل شده‌اند', - organizeBlocks: 'سازماندهی بلوک‌ها', change: 'تغییر', optional: '(اختیاری)', moveToThisNode: 'به این گره بروید', + selectNextStep: 'گام بعدی را انتخاب کنید', + changeBlock: 'تغییر گره', + organizeBlocks: 'گره‌ها را سازماندهی کنید', + addNextStep: 'مرحله بعدی را به این فرآیند اضافه کنید', + minimize: 'خروج از حالت تمام صفحه', + maximize: 'بیشینه‌سازی بوم', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retrySuccessful: 'امتحان مجدد با موفقیت انجام دهید', retryFailedTimes: '{{بار}} تلاش های مجدد ناموفق بود', }, + typeSwitch: {}, }, start: { required: 'الزامی', @@ -535,6 +540,10 @@ const translation = { title: 'وارد کردن از cURL', placeholder: 'رشته cURL را اینجا بچسبانید', }, + verifySSL: { + title: 'گواهی SSL را تأیید کنید', + warningTooltip: 'غیرفعال کردن تأیید SSL برای محیط‌های تولید توصیه نمی‌شود. این فقط باید در توسعه یا آزمایش استفاده شود، زیرا این کار اتصال را در معرض تهدیدات امنیتی مانند حملات میانی قرار می‌دهد.', + }, }, code: { inputVars: 'متغیرهای ورودی', @@ -667,6 +676,7 @@ const translation = { inputVars: 'متغیرهای ورودی', outputVars: { className: 'نام کلاس', + usage: 'اطلاعات استفاده از مدل', }, class: 'کلاس', classNamePlaceholder: 'نام کلاس خود را بنویسید', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'متغیر ورودی', + outputVars: { + isSuccess: 'موفقیت‌آمیز است. در صورت موفقیت مقدار 1 و در صورت شکست مقدار 0 است.', + errorReason: 'دلیل خطا', + usage: 'اطلاعات استفاده از مدل', + }, extractParameters: 'استخراج پارامترها', importFromTool: 'وارد کردن از ابزارها', addExtractParameter: 'افزودن پارامتر استخراج شده', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'تنظیمات پیشرفته', reasoningMode: 'حالت استدلال', reasoningModeTip: 'می‌توانید حالت استدلال مناسب را بر اساس توانایی مدل برای پاسخ به دستورات برای فراخوانی عملکردها یا پیشنهادات انتخاب کنید.', - isSuccess: 'موفقیت‌آمیز است. در صورت موفقیت مقدار 1 و در صورت شکست مقدار 0 است.', - errorReason: 'دلیل خطا', }, iteration: { deleteTitle: 'حذف نود تکرار؟', @@ -917,6 +930,35 @@ const translation = { restorationTip: 'پس از بازیابی نسخه، پیش‌نویس فعلی بازنویسی خواهد شد.', deletionTip: 'حذف غیرقابل برگشت است، لطفا تأیید کنید.', }, + debug: { + noData: { + runThisNode: 'این نود را اجرا کن', + description: 'نتایج آخرین اجرا در اینجا نمایش داده خواهد شد', + }, + variableInspect: { + trigger: { + clear: 'شفاف', + stop: 'متوقف کن، برو', + running: 'وضعیت اجرای کشینگ', + normal: 'بازبینی متغیر', + cached: 'مشاهده متغیرهای کش شده', + }, + chatNode: 'گفتگو', + edited: 'ویرایش شده', + systemNode: 'سیستم', + title: 'بازبینی متغیر', + clearAll: 'همه را بازنشانی کن', + emptyLink: 'بیشتر یاد بگیرید', + reset: 'تنظیم به آخرین مقدار اجرا شده', + view: 'مشاهده لاگ', + envNode: 'محیط زیست', + clearNode: 'کش متغیر کش شده را پاک کنید', + emptyTip: 'پس از عبور از یک گره روی بوم یا اجرای گره به صورت مرحله‌ای، می‌توانید مقدار فعلی متغیر گره را در بازرسی متغیر مشاهده کنید.', + resetConversationVar: 'متغیر گفتگو را به مقدار پیش‌فرض بازنشانی کنید', + }, + settingsTab: 'تنظیمات', + lastRunTab: 'آخرین اجرا', + }, } export default translation diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 6671092930..f3984c0435 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -317,6 +317,7 @@ const translation = { writeOpener: 'Écrire l\'introduction', placeholder: 'Rédigez votre message d\'ouverture ici, vous pouvez utiliser des variables, essayez de taper {{variable}}.', openingQuestion: 'Questions d\'ouverture', + openingQuestionPlaceholder: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}.', noDataPlaceHolder: 'Commencer la conversation avec l\'utilisateur peut aider l\'IA à établir une connexion plus proche avec eux dans les applications conversationnelles.', varTip: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index 5c0965815e..18cd04a1e1 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -93,6 +93,7 @@ const translation = { noIdeaTip: 'Pas d’idées ? Consultez nos modèles', optional: 'Optionnel', advancedShortDescription: 'Workflow amélioré pour conversations multi-tours', + dropDSLToCreateApp: 'Déposez le fichier DSL ici pour créer une application', }, editApp: 'Modifier les informations', editAppTitle: 'Modifier les informations de l\'application', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Configurez le fournisseur pour activer le traçage', moreProvider: 'Plus de fournisseurs', }, + arize: { + title: 'Arize', + description: 'Observabilité de LLM de niveau entreprise, évaluation en ligne et hors ligne, surveillance et expérimentation—alimentée par OpenTelemetry. Conçue spécialement pour les applications basées sur LLM et agents.', + }, + phoenix: { + title: 'Phoenix', + description: 'Plateforme open-source basée sur OpenTelemetry pour l’observabilité, l’évaluation, l’ingénierie des prompts et l’expérimentation de vos flux de travail et agents LLM.', + }, langsmith: { title: 'LangSmith', description: 'Une plateforme de développement tout-en-un pour chaque étape du cycle de vie des applications basées sur LLM.', @@ -163,6 +172,7 @@ const translation = { title: 'Tisser', description: 'Weave est une plateforme open-source pour évaluer, tester et surveiller les applications LLM.', }, + aliyun: {}, }, answerIcon: { description: 'S’il faut utiliser l’icône web app pour remplacer 🤖 dans l’application partagée', diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts index f08ef40c4d..23d6e43fc3 100644 --- a/web/i18n/fr-FR/common.ts +++ b/web/i18n/fr-FR/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'Échec du téléchargement. Veuillez réessayer plus tard.', more: 'Plus', downloadSuccess: 'Téléchargement terminé.', + deSelectAll: 'Désélectionner tout', + selectAll: 'Sélectionner tout', }, placeholder: { input: 'Veuillez entrer', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'Les extensions API fournissent une gestion centralisée des API, simplifiant la configuration pour une utilisation facile à travers les applications de Dify.', link: 'Apprenez comment développer votre propre Extension API.', - linkUrl: 'https://docs.dify.ai/fonctionnalites/extension/extension_basee_sur_api', add: 'Ajouter l\'extension API', selector: { title: 'Extension de l\'API', diff --git a/web/i18n/fr-FR/dataset-creation.ts b/web/i18n/fr-FR/dataset-creation.ts index 6339ceaac2..e1a5a85a8b 100644 --- a/web/i18n/fr-FR/dataset-creation.ts +++ b/web/i18n/fr-FR/dataset-creation.ts @@ -61,7 +61,6 @@ const translation = { preview: 'Aperçu', crawlSubPage: 'Explorer les sous-pages', configure: 'Configurer', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', maxDepth: 'Profondeur maximale', fireCrawlNotConfigured: 'Firecrawl n’est pas configuré', firecrawlTitle: 'Extraire du contenu web avec 🔥Firecrawl', @@ -88,7 +87,6 @@ const translation = { configureJinaReader: 'Configurer le lecteur Jina', configureWatercrawl: 'Configurer Watercrawl', waterCrawlNotConfigured: 'Watercrawl n\'est pas configuré', - watercrawlDocLink: 'https://docs.dify.ai/fr/guide/base-de-connaissances/créer-des-connaissances-et-télécharger-des-documents/importer-des-données-de-contenu/synchroniser-depuis-un-site-web', configureFirecrawl: 'Configurer Firecrawl', }, cancel: 'Annuler', diff --git a/web/i18n/fr-FR/dataset-documents.ts b/web/i18n/fr-FR/dataset-documents.ts index 7a795202ed..7960a3ed02 100644 --- a/web/i18n/fr-FR/dataset-documents.ts +++ b/web/i18n/fr-FR/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Supprimer', enableWarning: 'Le fichier archivé ne peut pas être activé', sync: 'Synchroniser', + pause: 'Pause', + resume: 'Reprendre', }, index: { enable: 'Activer', @@ -389,6 +391,8 @@ const translation = { searchResults_zero: 'RÉSULTAT', empty: 'Aucun Chunk trouvé', editChildChunk: 'Modifier le morceau enfant', + keywordDuplicate: 'Le mot-clé existe déjà', + keywordEmpty: 'Le mot-clé ne peut pas être vide.', }, } diff --git a/web/i18n/fr-FR/dataset.ts b/web/i18n/fr-FR/dataset.ts index c9739e20a1..a0858e998b 100644 --- a/web/i18n/fr-FR/dataset.ts +++ b/web/i18n/fr-FR/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'Le nom des métadonnées ne peut pas être vide', invalid: 'Le nom des métadonnées ne peut contenir que des lettres minuscules, des chiffres et des tirets bas et doit commencer par une lettre minuscule.', + tooLong: 'Le nom des métadonnées ne peut pas dépasser {{max}} caractères', }, batchEditMetadata: { editMetadata: 'Modifier les métadonnées', diff --git a/web/i18n/fr-FR/plugin.ts b/web/i18n/fr-FR/plugin.ts index 52930bd249..60366e28cf 100644 --- a/web/i18n/fr-FR/plugin.ts +++ b/web/i18n/fr-FR/plugin.ts @@ -53,14 +53,14 @@ const translation = { placeholder: 'Sélectionnez un outil...', params: 'CONFIGURATION DE RAISONNEMENT', unsupportedContent: 'La version du plugin installée ne fournit pas cette action.', - auto: 'Automatique', + auto: 'Auto', descriptionPlaceholder: 'Brève description de l’objectif de l’outil, par exemple, obtenir la température d’un endroit spécifique.', unsupportedContent2: 'Cliquez pour changer de version.', uninstalledTitle: 'Outil non installé', empty: 'Cliquez sur le bouton « + » pour ajouter des outils. Vous pouvez ajouter plusieurs outils.', toolLabel: 'Outil', settings: 'PARAMÈTRES UTILISATEUR', - paramsTip2: 'Lorsque « Automatique » est désactivé, la valeur par défaut est utilisée.', + paramsTip2: 'Lorsque « Auto » est désactivé, la valeur par défaut est utilisée.', paramsTip1: 'Contrôle les paramètres d’inférence LLM.', toolSetting: 'Paramètres de l\'outil', }, @@ -137,6 +137,7 @@ const translation = { next: 'Prochain', installPlugin: 'Installer le plugin', installFailedDesc: 'L’installation du plug-in a échoué.', + installWarning: 'Ce plugin n’est pas autorisé à être installé.', }, installFromGitHub: { installFailed: 'Échec de l’installation', @@ -193,7 +194,6 @@ const translation = { installing: 'Installation des plugins {{installingLength}}, 0 fait.', }, search: 'Rechercher', - submitPlugin: 'Soumettre le plugin', installAction: 'Installer', from: 'De', searchCategories: 'Catégories de recherche', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'La version actuelle de Dify n\'est pas compatible avec ce plugin, veuillez mettre à niveau vers la version minimale requise : {{minimalDifyVersion}}', requestAPlugin: 'Demander un plugin', + publishPlugins: 'Publier des plugins', } export default translation diff --git a/web/i18n/fr-FR/tools.ts b/web/i18n/fr-FR/tools.ts index 8f2362daf1..fdbe213df8 100644 --- a/web/i18n/fr-FR/tools.ts +++ b/web/i18n/fr-FR/tools.ts @@ -138,20 +138,96 @@ const translation = { howToGet: 'Comment obtenir', addToolModal: { type: 'type', - emptyTitle: 'Aucun outil de flux de travail disponible', added: 'supplémentaire', add: 'ajouter', category: 'catégorie', manageInTools: 'Gérer dans Outils', - emptyTip: 'Allez dans « Flux de travail -> Publier en tant qu’outil »', - emptyTitleCustom: 'Aucun outil personnalisé disponible', - emptyTipCustom: 'Créer un outil personnalisé', + custom: { + title: 'Aucun outil personnalisé disponible', + tip: 'Créer un outil personnalisé', + }, + workflow: { + title: 'Aucun outil de workflow disponible', + tip: 'Publier des workflows en tant qu\'outils dans le Studio', + }, + mcp: { + title: 'Aucun outil MCP disponible', + tip: 'Ajouter un serveur MCP', + }, + agent: { + title: 'Aucune stratégie d\'agent disponible', + }, }, openInStudio: 'Ouvrir dans Studio', customToolTip: 'En savoir plus sur les outils personnalisés Dify', toolNameUsageTip: 'Nom de l’appel de l’outil pour le raisonnement et l’invite de l’agent', copyToolName: 'Copier le nom', noTools: 'Aucun outil trouvé', + mcp: { + create: { + cardTitle: 'Ajouter un Serveur MCP (HTTP)', + cardLink: 'En savoir plus sur l\'intégration du serveur MCP', + }, + noConfigured: 'Serveur Non Configuré', + updateTime: 'Mis à jour', + toolsCount: '{count} outils', + noTools: 'Aucun outil disponible', + modal: { + title: 'Ajouter un Serveur MCP (HTTP)', + editTitle: 'Modifier le Serveur MCP (HTTP)', + name: 'Nom & Icône', + namePlaceholder: 'Nommez votre serveur MCP', + serverUrl: 'URL du Serveur', + serverUrlPlaceholder: 'URL de l\'endpoint du serveur', + serverUrlWarning: 'Mettre à jour l\'adresse du serveur peut perturber les applications qui dépendent de ce serveur', + serverIdentifier: 'Identifiant du Serveur', + serverIdentifierTip: 'Identifiant unique pour le serveur MCP au sein de l\'espace de travail. Seulement des lettres minuscules, des chiffres, des traits de soulignement et des tirets. Jusqu\'à 24 caractères.', + serverIdentifierPlaceholder: 'Identifiant unique, par ex. mon-serveur-mcp', + serverIdentifierWarning: 'Le serveur ne sera pas reconnu par les applications existantes après un changement d\'ID', + cancel: 'Annuler', + save: 'Enregistrer', + confirm: 'Ajouter & Authoriser', + }, + delete: 'Supprimer le Serveur MCP', + deleteConfirmTitle: 'Souhaitez-vous supprimer {mcp}?', + operation: { + edit: 'Modifier', + remove: 'Supprimer', + }, + authorize: 'Autoriser', + authorizing: 'Autorisation en cours...', + authorizingRequired: 'L\'autorisation est requise', + authorizeTip: 'Après autorisation, les outils seront affichés ici.', + update: 'Mettre à jour', + updating: 'Mise à jour en cours', + gettingTools: 'Obtention des Outils...', + updateTools: 'Mise à jour des Outils...', + toolsEmpty: 'Outils non chargés', + getTools: 'Obtenir des outils', + toolUpdateConfirmTitle: 'Mettre à jour la Liste des Outils', + toolUpdateConfirmContent: 'La mise à jour de la liste des outils peut affecter les applications existantes. Souhaitez-vous continuer?', + toolsNum: '{count} outils inclus', + onlyTool: '1 outil inclus', + identifier: 'Identifiant du Serveur (Cliquez pour Copier)', + server: { + title: 'Serveur MCP', + url: 'URL du Serveur', + reGen: 'Voulez-vous régénérer l\'URL du serveur?', + addDescription: 'Ajouter une description', + edit: 'Modifier la description', + modal: { + addTitle: 'Ajouter une description pour activer le serveur MCP', + editTitle: 'Modifier la description', + description: 'Description', + descriptionPlaceholder: 'Expliquez ce que fait cet outil et comment il doit être utilisé par le LLM', + parameters: 'Paramètres', + parametersTip: 'Ajoutez des descriptions pour chaque paramètre afin d\'aider le LLM à comprendre leur objectif et leurs contraintes.', + parametersPlaceholder: 'Objectif et contraintes du paramètre', + confirm: 'Activer le Serveur MCP', + }, + publishTip: 'Application non publiée. Merci de publier l\'application en premier.', + }, + }, } export default translation diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 7a15bcaa4d..8c8180abff 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Définir la valeur de la variable', needConnectTip: 'Cette étape n\'est connectée à rien', maxTreeDepth: 'Limite maximale de {{depth}} nœuds par branche', - needEndNode: 'Le bloc de fin doit être ajouté', - needAnswerNode: 'Le bloc de réponse doit être ajouté', workflowProcess: 'Processus de flux de travail', notRunning: 'Pas encore en cours d\'exécution', previewPlaceholder: 'Entrez le contenu dans la boîte ci-dessous pour commencer à déboguer le Chatbot', @@ -58,7 +56,6 @@ const translation = { learnMore: 'En savoir plus', copy: 'Copier', duplicate: 'Dupliquer', - addBlock: 'Ajouter un bloc', pasteHere: 'Coller ici', pointerMode: 'Mode pointeur', handMode: 'Mode main', @@ -115,6 +112,9 @@ const translation = { referenceVar: 'Variable de référence', exportImage: 'Exporter l\'image', exportJPEG: 'Exporter en JPEG', + needEndNode: 'Le nœud de fin doit être ajouté', + needAnswerNode: 'Le nœud de réponse doit être ajouté.', + addBlock: 'Ajouter un nœud', }, env: { envPanelTitle: 'Variables d\'Environnement', @@ -129,6 +129,8 @@ const translation = { value: 'valeur', valuePlaceholder: 'Valeur de l\'env', secretTip: 'Utilisé pour définir des informations ou des données sensibles, avec des paramètres DSL configurés pour la prévention des fuites.', + description: 'Description', + descriptionPlaceholder: 'Décrivez la variable', }, export: { title: 'Exporter des variables d\'environnement secrètes?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} pas en avant', sessionStart: 'Début de la session', currentState: 'État actuel', - nodeTitleChange: 'Titre du bloc modifié', - nodeDescriptionChange: 'Description du bloc modifiée', - nodeDragStop: 'Bloc déplacé', - nodeChange: 'Bloc modifié', - nodeConnect: 'Bloc connecté', - nodePaste: 'Bloc collé', - nodeDelete: 'Bloc supprimé', - nodeAdd: 'Bloc ajouté', - nodeResize: 'Bloc redimensionné', noteAdd: 'Note ajoutée', noteChange: 'Note modifiée', noteDelete: 'Note supprimée', - edgeDelete: 'Bloc déconnecté', + nodeConnect: 'Node connecté', + nodeChange: 'Nœud changé', + nodeResize: 'Nœud redimensionné', + edgeDelete: 'Nœud déconnecté', + nodeDelete: 'Nœud supprimé', + nodePaste: 'Node collé', + nodeDragStop: 'Nœud déplacé', + nodeTitleChange: 'Titre du nœud modifié', + nodeAdd: 'Nœud ajouté', + nodeDescriptionChange: 'La description du nœud a changé', }, errorMsg: { fieldRequired: '{{field}} est requis', @@ -217,8 +219,6 @@ const translation = { loop: 'Boucle', }, tabs: { - 'searchBlock': 'Rechercher un bloc', - 'blocks': 'Blocs', 'tools': 'Outils', 'allTool': 'Tous', 'builtInTool': 'Intégré', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Outil de recherche', 'plugin': 'Plugin', 'agent': 'Stratégie d’agent', + 'blocks': 'Nœuds', + 'searchBlock': 'Nœud de recherche', }, blocks: { 'start': 'Début', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Champ de saisie de l\'utilisateur', - changeBlock: 'Changer de bloc', helpLink: 'Lien d\'aide', about: 'À propos', createdBy: 'Créé par', nextStep: 'Étape suivante', - addNextStep: 'Ajouter le prochain bloc dans ce flux de travail', - selectNextStep: 'Sélectionner le prochain bloc', runThisStep: 'Exécuter cette étape', checklist: 'Liste de contrôle', checklistTip: 'Assurez-vous que tous les problèmes sont résolus avant de publier', checklistResolved: 'Tous les problèmes ont été résolus', - organizeBlocks: 'Organiser les blocs', change: 'Modifier', optional: '(facultatif)', moveToThisNode: 'Déplacer vers ce nœud', + organizeBlocks: 'Organiser les nœuds', + addNextStep: 'Ajoutez la prochaine étape dans ce flux de travail', + selectNextStep: 'Sélectionner la prochaine étape', + changeBlock: 'Changer de nœud', + maximize: 'Maximiser le Canvas', + minimize: 'Sortir du mode plein écran', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { ms: 'ms', retries: '{{num}} Tentatives', }, + typeSwitch: {}, }, start: { required: 'requis', @@ -464,6 +469,7 @@ const translation = { options: { disabled: { subTitle: 'Ne pas activer le filtrage des métadonnées', + title: 'Handicapé', }, automatic: { subTitle: 'Générer automatiquement des conditions de filtrage des métadonnées en fonction de la requête de l\'utilisateur', @@ -534,6 +540,10 @@ const translation = { placeholder: 'Collez la chaîne cURL ici', title: 'Importer à partir de cURL', }, + verifySSL: { + title: 'Vérifier le certificat SSL', + warningTooltip: 'Désactiver la vérification SSL n\'est pas recommandé pour les environnements de production. Cela ne devrait être utilisé que dans le développement ou les tests, car cela rend la connexion vulnérable aux menaces de sécurité telles que les attaques de type \'man-in-the-middle\'.', + }, }, code: { inputVars: 'Variables de saisie', @@ -666,6 +676,7 @@ const translation = { inputVars: 'Variables de saisie', outputVars: { className: 'Nom de la classe', + usage: 'Informations sur l\'utilisation du modèle', }, class: 'Classe', classNamePlaceholder: 'Écrivez le nom de votre classe', @@ -679,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Variable de saisie', + outputVars: { + isSuccess: 'Est réussi. En cas de succès, la valeur est 1, en cas d\'échec, la valeur est 0.', + errorReason: 'Raison de l\'erreur', + usage: 'Informations sur l\'utilisation du modèle', + }, extractParameters: 'Extraire des paramètres', importFromTool: 'Importer des outils', addExtractParameter: 'Ajouter un paramètre d\'extraction', @@ -698,8 +714,6 @@ const translation = { advancedSetting: 'Paramètre avancé', reasoningMode: 'Mode de raisonnement', reasoningModeTip: 'Vous pouvez choisir le mode de raisonnement approprié en fonction de la capacité du modèle à répondre aux instructions pour les appels de fonction ou les invites.', - isSuccess: 'Est réussi. En cas de succès, la valeur est 1, en cas d\'échec, la valeur est 0.', - errorReason: 'Raison de l\'erreur', }, iteration: { deleteTitle: 'Supprimer le nœud d\'itération?', @@ -916,6 +930,35 @@ const translation = { deletionTip: 'La suppression est irreversible, veuillez confirmer.', latest: 'Dernier', }, + debug: { + noData: { + description: 'Les résultats de la dernière exécution seront affichés ici', + runThisNode: 'Exécutez ce nœud', + }, + variableInspect: { + trigger: { + clear: 'Clair', + cached: 'Afficher les variables mises en cache', + running: 'État d\'exécution du cache', + stop: 'Arrête de courir', + normal: 'Inspection de Variable', + }, + title: 'Inspection de Variable', + clearAll: 'Réinitialiser tout', + envNode: 'Environnement', + clearNode: 'Effacer la variable mise en cache', + view: 'Voir le journal', + systemNode: 'Système', + reset: 'Réinitialiser à la dernière valeur d\'exécution', + chatNode: 'Conversation', + emptyLink: 'En savoir plus', + edited: 'Édité', + resetConversationVar: 'Réinitialiser la variable de conversation à la valeur par défaut', + emptyTip: 'Après avoir dessiné un nœud sur le canevas ou exécuté un nœud étape par étape, vous pouvez voir la valeur actuelle de la variable du nœud dans l\'Inspecteur de Variables.', + }, + settingsTab: 'Paramètres', + lastRunTab: 'Dernière Exécution', + }, } export default translation diff --git a/web/i18n/hi-IN/app-debug.ts b/web/i18n/hi-IN/app-debug.ts index 3f4b06c08b..ded2af4132 100644 --- a/web/i18n/hi-IN/app-debug.ts +++ b/web/i18n/hi-IN/app-debug.ts @@ -362,6 +362,7 @@ const translation = { placeholder: 'यहां अपना प्रारंभक संदेश लिखें, आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करने का प्रयास करें।', openingQuestion: 'प्रारंभिक प्रश्न', + openingQuestionPlaceholder: 'आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करके देखें।', noDataPlaceHolder: 'उपयोगकर्ता के साथ संवाद प्रारंभ करने से एआई को संवादात्मक अनुप्रयोगों में उनके साथ निकट संबंध स्थापित करने में मदद मिल सकती है।', varTip: diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index 3929dfeb6a..e9073722f7 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -93,6 +93,7 @@ const translation = { advancedShortDescription: 'बहु-चरण वार्तालाप के लिए उन्नत वर्कफ़्लो', noTemplateFoundTip: 'विभिन्न कीवर्ड का उपयोग करके खोजने का प्रयास करें।', workflowUserDescription: 'ड्रैग-एंड-ड्रॉप सरलता के साथ स्वायत्त AI वर्कफ़्लो का दृश्य निर्माण करें।', + dropDSLToCreateApp: 'यहाँ DSL फ़ाइल ड्रॉप करें ताकि ऐप बनाया जा सके', }, editApp: 'जानकारी संपादित करें', editAppTitle: 'ऐप जानकारी संपादित करें', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'ट्रेसिंग सक्षम करने के लिए प्रदाता कॉन्फ़िगर करें', moreProvider: 'अधिक प्रदाता', }, + arize: { + title: 'Arize', + description: 'एंटरप्राइज-स्तरीय LLM ऑब्ज़र्वेबिलिटी, ऑनलाइन और ऑफ़लाइन मूल्यांकन, मॉनिटरिंग और प्रयोग — OpenTelemetry द्वारा समर्थित। LLM और एजेंट-आधारित अनुप्रयोगों के लिए विशेष रूप से तैयार किया गया।', + }, + phoenix: { + title: 'Phoenix', + description: 'आपके LLM वर्कफ़्लोज़ और एजेंट्स के लिए ओपन-सोर्स और OpenTelemetry-आधारित ऑब्ज़र्वेबिलिटी, मूल्यांकन, प्रॉम्प्ट इंजीनियरिंग और प्रयोग का प्लेटफ़ॉर्म।', + }, langsmith: { title: 'LangSmith', description: 'LLM-संचालित एप्लिकेशन जीवनचक्र के प्रत्येक चरण के लिए एक ऑल-इन-वन डेवलपर प्लेटफ़ॉर्म।', @@ -163,6 +172,7 @@ const translation = { title: 'बुनना', description: 'वीव एक ओपन-सोर्स प्लेटफ़ॉर्म है जो LLM अनुप्रयोगों का मूल्यांकन, परीक्षण और निगरानी करने के लिए है।', }, + aliyun: {}, }, answerIcon: { title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें', diff --git a/web/i18n/hi-IN/common.ts b/web/i18n/hi-IN/common.ts index 65565f9955..4efc698d7c 100644 --- a/web/i18n/hi-IN/common.ts +++ b/web/i18n/hi-IN/common.ts @@ -58,6 +58,8 @@ const translation = { downloadSuccess: 'डाउनलोड पूरा हुआ।', downloadFailed: 'डाउनलोड विफल। कृपया बाद में पुनः प्रयास करें।', format: 'फॉर्मेट', + selectAll: 'सभी चुनें', + deSelectAll: 'सभी चयन हटाएँ', }, errorMsg: { fieldRequired: '{{field}} आवश्यक है', @@ -488,7 +490,6 @@ const translation = { title: 'एपीआई एक्सटेंशन केंद्रीकृत एपीआई प्रबंधन प्रदान करते हैं, जो Dify के अनुप्रयोगों में आसान उपयोग के लिए कॉन्फ़िगरेशन को सरल बनाते हैं।', link: 'अपना खुद का एपीआई एक्सटेंशन कैसे विकसित करें, यह जानें।', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'एपीआई एक्सटेंशन जोड़ें', selector: { title: 'एपीआई एक्सटेंशन', diff --git a/web/i18n/hi-IN/dataset-creation.ts b/web/i18n/hi-IN/dataset-creation.ts index 1f8384354d..64aff7193f 100644 --- a/web/i18n/hi-IN/dataset-creation.ts +++ b/web/i18n/hi-IN/dataset-creation.ts @@ -65,8 +65,6 @@ const translation = { run: 'चलाएं', firecrawlTitle: '🔥फायरक्रॉल के साथ वेब सामग्री निकालें', firecrawlDoc: 'फायरक्रॉल दस्तावेज़', - firecrawlDocLink: - 'https://docs.dify.ai/guides/knowledge-base/sync_from_website', options: 'विकल्प', crawlSubPage: 'उप-पृष्ठों को क्रॉल करें', limit: 'सीमा', @@ -97,7 +95,6 @@ const translation = { configureFirecrawl: 'फायरक्रॉल को कॉन्फ़िगर करें', watercrawlDoc: 'वाटरक्रॉल दस्तावेज़', waterCrawlNotConfiguredDescription: 'इसे उपयोग करने के लिए वॉटरक्रॉल को एपीआई कुंजी के साथ कॉन्फ़िगर करें।', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureJinaReader: 'जिना रीडर कॉन्फ़िगर करें', configureWatercrawl: 'वाटरक्रॉल कॉन्फ़िगर करें', }, diff --git a/web/i18n/hi-IN/dataset-documents.ts b/web/i18n/hi-IN/dataset-documents.ts index 35bcb0aad2..4713ea1a89 100644 --- a/web/i18n/hi-IN/dataset-documents.ts +++ b/web/i18n/hi-IN/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'हटाएँ', enableWarning: 'संग्रहित फाइल को सक्रिय नहीं किया जा सकता', sync: 'सिंक्रोनाइज़ करें', + resume: 'रिज़्यूमे', + pause: 'रोकें', }, index: { enable: 'सक्रिय करें', @@ -390,6 +392,8 @@ const translation = { chunkAdded: '1 हिस्सा जोड़ा गया', chunkDetail: 'चंक विवरण', regenerationConfirmMessage: 'चाइल्ड चंक्स को रीजनरेट करने से वर्तमान चाइल्ड चंक्स ओवरराइट हो जाएंगे, जिसमें संपादित चंक्स और नए जोड़े गए चंक्स शामिल हैं। पुनरुत्थान को पूर्ववत नहीं किया जा सकता है।', + keywordDuplicate: 'कीवर्ड पहले से मौजूद है', + keywordEmpty: 'कीवर्ड ख़ाली नहीं हो सकता', }, } diff --git a/web/i18n/hi-IN/dataset.ts b/web/i18n/hi-IN/dataset.ts index 9be333cdec..e94c777718 100644 --- a/web/i18n/hi-IN/dataset.ts +++ b/web/i18n/hi-IN/dataset.ts @@ -186,6 +186,7 @@ const translation = { checkName: { empty: 'मेटाडाटा का नाम खाली नहीं हो सकता', invalid: 'मेटाडेटा नाम में केवल छोटे अक्षर, संख्या और अंडरस्कोर शामिल हो सकते हैं और इसे छोटे अक्षर से शुरू होना चाहिए।', + tooLong: 'मेटाडेटा नाम {{max}} वर्णों से अधिक नहीं हो सकता', }, batchEditMetadata: { editMetadata: 'मेटाडेटा संपादित करें', diff --git a/web/i18n/hi-IN/plugin.ts b/web/i18n/hi-IN/plugin.ts index 76eb84246a..0834f5aa07 100644 --- a/web/i18n/hi-IN/plugin.ts +++ b/web/i18n/hi-IN/plugin.ts @@ -137,6 +137,7 @@ const translation = { installFailedDesc: 'प्लगइन स्थापित करने में विफल रहा।', installedSuccessfullyDesc: 'प्लगइन सफलतापूर्वक स्थापित किया गया है।', fromTrustSource: 'कृपया सुनिश्चित करें कि आप केवल एक <trustSource>विश्वसनीय स्रोत</trustSource> से प्लगइन्स स्थापित करें।', + installWarning: 'इस प्लगइन को स्थापित करने की अनुमति नहीं है।', }, installFromGitHub: { gitHubRepo: 'गिटहब रिपॉजिटरी', @@ -196,7 +197,6 @@ const translation = { fromMarketplace: 'मार्केटप्लेस से', searchPlugins: 'खोज प्लगइन्स', install: '{{num}} इंस्टॉलेशन', - submitPlugin: 'प्लगइन सबमिट करें', allCategories: 'सभी श्रेणियाँ', search: 'खोज', searchTools: 'खोज उपकरण...', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'वर्तमान डिफाई संस्करण इस प्लगइन के साथ संगत नहीं है, कृपया आवश्यक न्यूनतम संस्करण में अपग्रेड करें: {{minimalDifyVersion}}', requestAPlugin: 'एक प्लगइन का अनुरोध करें', + publishPlugins: 'प्लगइन प्रकाशित करें', } export default translation diff --git a/web/i18n/hi-IN/tools.ts b/web/i18n/hi-IN/tools.ts index 105e7e5fa6..8f721da44e 100644 --- a/web/i18n/hi-IN/tools.ts +++ b/web/i18n/hi-IN/tools.ts @@ -29,10 +29,21 @@ const translation = { add: 'जोड़ें', added: 'जोड़ा गया', manageInTools: 'उपकरणों में प्रबंधित करें', - emptyTitle: 'कोई कार्यप्रवाह उपकरण उपलब्ध नहीं', - emptyTip: 'कार्यप्रवाह -> उपकरण के रूप में प्रकाशित पर जाएं', - emptyTipCustom: 'एक कस्टम टूल बनाएं', - emptyTitleCustom: 'कोई कस्टम टूल उपलब्ध नहीं है', + custom: { + title: 'कोई कस्टम टूल उपलब्ध नहीं है', + tip: 'एक कस्टम टूल बनाएं', + }, + workflow: { + title: 'कोई वर्कफ़्लो टूल उपलब्ध नहीं है', + tip: 'स्टूडियो में टूल के रूप में वर्कफ़्लो प्रकाशित करें', + }, + mcp: { + title: 'कोई MCP टूल उपलब्ध नहीं है', + tip: 'एक MCP सर्वर जोड़ें', + }, + agent: { + title: 'कोई एजेंट रणनीति उपलब्ध नहीं है', + }, }, createTool: { title: 'कस्टम उपकरण बनाएं', @@ -157,6 +168,71 @@ const translation = { toolNameUsageTip: 'एजेंट तर्क और प्रेरण के लिए उपकरण कॉल नाम', noTools: 'कोई उपकरण नहीं मिला', copyToolName: 'नाम कॉपी करें', + mcp: { + create: { + cardTitle: 'MCP सर्वर जोड़ें (HTTP)', + cardLink: 'MCP सर्वर एकीकरण के बारे में अधिक जानें', + }, + noConfigured: 'कॉन्फ़िगर न किया गया सर्वर', + updateTime: 'अपडेट किया गया', + toolsCount: '{count} टूल्स', + noTools: 'कोई टूल उपलब्ध नहीं', + modal: { + title: 'MCP सर्वर जोड़ें (HTTP)', + editTitle: 'MCP सर्वर संपादित करें (HTTP)', + name: 'नाम और आइकन', + namePlaceholder: 'अपने MCP सर्वर को नाम दें', + serverUrl: 'सर्वर URL', + serverUrlPlaceholder: 'सर्वर एंडपॉइंट का URL', + serverUrlWarning: 'सर्वर पता अपडेट करने से इस सर्वर पर निर्भर एप्लिकेशन बाधित हो सकते हैं', + serverIdentifier: 'सर्वर आईडेंटिफ़ायर', + serverIdentifierTip: 'वर्कस्पेस में MCP सर्वर के लिए अद्वितीय आईडेंटिफ़ायर। केवल लोअरकेस अक्षर, संख्याएँ, अंडरस्कोर और हाइफ़न। अधिकतम 24 वर्ण।', + serverIdentifierPlaceholder: 'अद्वितीय आईडेंटिफ़ायर, उदा. my-mcp-server', + serverIdentifierWarning: 'आईडी बदलने के बाद सर्वर को मौजूदा ऐप्स द्वारा पहचाना नहीं जाएगा', + cancel: 'रद्द करें', + save: 'सहेजें', + confirm: 'जोड़ें और अधिकृत करें', + }, + delete: 'MCP सर्वर हटाएँ', + deleteConfirmTitle: '{mcp} हटाना चाहते हैं?', + operation: { + edit: 'संपादित करें', + remove: 'हटाएँ', + }, + authorize: 'अधिकृत करें', + authorizing: 'अधिकृत किया जा रहा है...', + authorizingRequired: 'प्राधिकरण आवश्यक है', + authorizeTip: 'अधिकृत होने के बाद, टूल यहाँ प्रदर्शित होंगे।', + update: 'अपडेट करें', + updating: 'अपडेट हो रहा है...', + gettingTools: 'टूल्स प्राप्त किए जा रहे हैं...', + updateTools: 'टूल्स अपडेट किए जा रहे हैं...', + toolsEmpty: 'टूल्स लोड नहीं हुए', + getTools: 'टूल्स प्राप्त करें', + toolUpdateConfirmTitle: 'टूल सूची अपडेट करें', + toolUpdateConfirmContent: 'टूल सूची अपडेट करने से मौजूदा ऐप्स प्रभावित हो सकते हैं। आगे बढ़ना चाहते हैं?', + toolsNum: '{count} टूल्स शामिल', + onlyTool: '1 टूल शामिल', + identifier: 'सर्वर आईडेंटिफ़ायर (कॉपी करने के लिए क्लिक करें)', + server: { + title: 'MCP सर्वर', + url: 'सर्वर URL', + reGen: 'सर्वर URL पुनः उत्पन्न करना चाहते हैं?', + addDescription: 'विवरण जोड़ें', + edit: 'विवरण संपादित करें', + modal: { + addTitle: 'MCP सर्वर सक्षम करने के लिए विवरण जोड़ें', + editTitle: 'विवरण संपादित करें', + description: 'विवरण', + descriptionPlaceholder: 'समझाएं कि यह टूल क्या करता है और LLM द्वारा इसका उपयोग कैसे किया जाना चाहिए', + parameters: 'पैरामीटर्स', + parametersTip: 'प्रत्येक पैरामीटर के लिए विवरण जोड़ें ताकि LLM को उनके उद्देश्य और बाधाओं को समझने में मदद मिले।', + parametersPlaceholder: 'पैरामीटर उद्देश्य और बाधाएँ', + confirm: 'MCP सर्वर सक्षम करें', + }, + publishTip: 'ऐप प्रकाशित नहीं हुआ। कृपया पहले ऐप प्रकाशित करें।', + }, + }, } export default translation diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index c295b16603..ffddacaf3a 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'वेरिएबल सेट करें', needConnectTip: 'यह चरण किसी से जुड़ा नहीं है', maxTreeDepth: 'प्रति शाखा अधिकतम {{depth}} नोड्स की सीमा', - needEndNode: 'अंत ब्लॉक जोड़ा जाना चाहिए', - needAnswerNode: 'उत्तर ब्लॉक जोड़ा जाना चाहिए', workflowProcess: 'कार्यप्रवाह प्रक्रिया', notRunning: 'अभी तक नहीं चल रहा', previewPlaceholder: @@ -60,7 +58,6 @@ const translation = { learnMore: 'अधिक जानें', copy: 'कॉपी करें', duplicate: 'डुप्लिकेट करें', - addBlock: 'ब्लॉक जोड़ें', pasteHere: 'यहां पेस्ट करें', pointerMode: 'पॉइंटर मोड', handMode: 'हैंड मोड', @@ -118,6 +115,9 @@ const translation = { publishUpdate: 'अपडेट प्रकाशित करें', exportSVG: 'SVG के रूप में निर्यात करें', versionHistory: 'संस्करण इतिहास', + needAnswerNode: 'उत्तर नोड जोड़ा जाना चाहिए', + addBlock: 'नोड जोड़ें', + needEndNode: 'अंत नोड जोड़ा जाना चाहिए', }, env: { envPanelTitle: 'पर्यावरण चर', @@ -132,6 +132,8 @@ const translation = { value: 'मान', valuePlaceholder: 'पर्यावरण मान', secretTip: 'संवेदनशील जानकारी या डेटा को परिभाषित करने के लिए उपयोग किया जाता है, DSL सेटिंग्स लीक रोकथाम के लिए कॉन्फ़िगर की गई हैं।', + description: 'विवरण', + descriptionPlaceholder: 'चर का वर्णन करें', }, export: { title: 'गुप्त पर्यावरण चर निर्यात करें?', @@ -179,19 +181,19 @@ const translation = { stepForward_other: '{{count}} कदम आगे', sessionStart: 'सत्र प्रारंभ', currentState: 'वर्तमान स्थिति', - nodeTitleChange: 'ब्लॉक शीर्षक बदला गया', - nodeDescriptionChange: 'ब्लॉक विवरण बदला गया', - nodeDragStop: 'ब्लॉक स्थानांतरित किया गया', - nodeChange: 'ब्लॉक बदला गया', - nodeConnect: 'ब्लॉक कनेक्ट किया गया', - nodePaste: 'ब्लॉक पेस्ट किया गया', - nodeDelete: 'ब्लॉक हटाया गया', - nodeAdd: 'ब्लॉक जोड़ा गया', - nodeResize: 'ब्लॉक का आकार बदला गया', noteAdd: 'नोट जोड़ा गया', noteChange: 'नोट बदला गया', noteDelete: 'नोट हटाया गया', - edgeDelete: 'ब्लॉक डिस्कनेक्ट किया गया', + nodeConnect: 'नोड कनेक्टेड', + nodeResize: 'नोड का आकार बदला गया', + nodeDelete: 'नोड हटा दिया गया', + nodeDragStop: 'नोड स्थानांतरित किया गया', + nodeChange: 'नोड बदला गया', + nodeAdd: 'नोड जोड़ा गया', + nodeTitleChange: 'नोड का शीर्षक बदल दिया गया', + edgeDelete: 'नोड डिस्कनेक्ट हो गया', + nodePaste: 'नोड चिपका हुआ', + nodeDescriptionChange: 'नोड का वर्णन बदल गया', }, errorMsg: { fieldRequired: '{{field}} आवश्यक है', @@ -220,8 +222,6 @@ const translation = { loop: 'लूप', }, tabs: { - 'searchBlock': 'ब्लॉक खोजें', - 'blocks': 'ब्लॉक्स', 'tools': 'टूल्स', 'allTool': 'सभी', 'builtInTool': 'अंतर्निहित', @@ -235,6 +235,8 @@ const translation = { 'searchTool': 'खोज उपकरण', 'plugin': 'प्लगइन', 'agent': 'एजेंट रणनीति', + 'searchBlock': 'खोज नोड', + 'blocks': 'नोड्स', }, blocks: { 'start': 'प्रारंभ', @@ -299,22 +301,24 @@ const translation = { }, panel: { userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड', - changeBlock: 'ब्लॉक बदलें', helpLink: 'सहायता लिंक', about: 'के बारे में', createdBy: 'द्वारा बनाया गया ', nextStep: 'अगला कदम', - addNextStep: 'इस वर्कफ़्लो में अगला ब्लॉक जोड़ें', - selectNextStep: 'अगला ब्लॉक चुनें', runThisStep: 'इस कदम को चलाएं', checklist: 'चेकलिस्ट', checklistTip: 'प्रकाशित करने से पहले सुनिश्चित करें कि सभी समस्याएं हल हो गई हैं', checklistResolved: 'सभी समस्याएं हल हो गई हैं', - organizeBlocks: 'ब्लॉक्स को व्यवस्थित करें', change: 'बदलें', optional: '(वैकल्पिक)', moveToThisNode: 'इस नोड पर जाएं', + changeBlock: 'नोड बदलें', + addNextStep: 'इस कार्यप्रवाह में अगला कदम जोड़ें', + selectNextStep: 'अगला कदम चुनें', + organizeBlocks: 'नोड्स का आयोजन करें', + minimize: 'पूर्ण स्क्रीन से बाहर निकलें', + maximize: 'कैनवास का अधिकतम लाभ उठाएँ', }, nodes: { common: { @@ -372,6 +376,7 @@ const translation = { retry: 'पुनर्प्रयास', retryOnFailure: 'विफलता पर पुनः प्रयास करें', }, + typeSwitch: {}, }, start: { required: 'आवश्यक', @@ -548,6 +553,10 @@ const translation = { placeholder: 'यहां cURL स्ट्रिंग पेस्ट करें', title: 'cURL से आयात करें', }, + verifySSL: { + title: 'SSL प्रमाणपत्र की पुष्टि करें', + warningTooltip: 'SSL सत्यापन को अक्षम करना उत्पादन वातावरण के लिए अनुशंसित नहीं है। इसका उपयोग केवल विकास या परीक्षण में किया जाना चाहिए, क्योंकि यह कनेक्शन को मिडल-मैन हमलों जैसे सुरक्षा खतरों के लिए कमजोर बना देता है।', + }, }, code: { inputVars: 'इनपुट वेरिएबल्स', @@ -683,6 +692,7 @@ const translation = { inputVars: 'इनपुट वेरिएबल्स', outputVars: { className: 'क्लास नाम', + usage: 'मॉडल उपयोग जानकारी', }, class: 'क्लास', classNamePlaceholder: 'अपना क्लास नाम लिखें', @@ -697,6 +707,11 @@ const translation = { }, parameterExtractor: { inputVar: 'इनपुट वेरिएबल', + outputVars: { + isSuccess: 'सफलता है। सफलता पर मान 1 है, असफलता पर मान 0 है।', + errorReason: 'त्रुटि का कारण', + usage: 'मॉडल उपयोग जानकारी', + }, extractParameters: 'पैरामीटर्स निकालें', importFromTool: 'उपकरणों से आयात करें', addExtractParameter: 'एक्सट्रेक्ट पैरामीटर जोड़ें', @@ -719,8 +734,6 @@ const translation = { reasoningMode: 'रीज़निंग मोड', reasoningModeTip: 'फ़ंक्शन कॉलिंग या प्रॉम्प्ट्स के लिए निर्देशों का जवाब देने की मॉडल की क्षमता के आधार पर उपयुक्त रीज़निंग मोड चुन सकते हैं।', - isSuccess: 'सफलता है। सफलता पर मान 1 है, असफलता पर मान 0 है।', - errorReason: 'त्रुटि का कारण', }, iteration: { deleteTitle: 'इटरेशन नोड हटाएं?', @@ -937,6 +950,35 @@ const translation = { defaultName: 'अविभाजित संस्करण', deletionTip: 'हटाना अप्रतिबंधी है, कृपया पुष्टि करें।', }, + debug: { + noData: { + runThisNode: 'इस नोड को चलाएँ', + description: 'अंतिम दौड़ के परिणाम यहाँ प्रदर्शित किए जाएंगे', + }, + variableInspect: { + trigger: { + clear: 'स्पष्ट', + stop: 'रुको दौड़', + running: 'कैशिंग चल रहा स्थिति', + normal: 'चर चरखा', + cached: 'कैश की गई परिवर्तनीयताओं को देखें', + }, + emptyLink: 'और जानें', + systemNode: 'प्रणाली', + chatNode: 'संवाद', + reset: 'अंतिम रन मान पर रीसेट करें', + view: 'लॉग देखें', + clearAll: 'सभी रीसेट करें', + title: 'चर चर के निरीक्षण', + edited: 'संशोधित किया गया', + envNode: 'पर्यावरण', + clearNode: 'कैश की गई वैरिएबल को साफ करें', + resetConversationVar: 'संवाद चर को डिफ़ॉल्ट मान पर रीसेट करें', + emptyTip: 'कैनवास पर एक नोड पर कदम रखने के बाद या चरण दर चरण एक नोड चलाने के बाद, आप वेरिएबल इंस्पेक्ट में नोड वेरिएबल का वर्तमान मान देख सकते हैं।', + }, + settingsTab: 'सेटिंग्स', + lastRunTab: 'अंतिम रन', + }, } export default translation diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index c8b3c08302..bfa75b282b 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -365,6 +365,7 @@ const translation = { placeholder: 'Scrivi qui il tuo messaggio introduttivo, puoi usare variabili, prova a scrivere {{variable}}.', openingQuestion: 'Domande iniziali', + openingQuestionPlaceholder: 'Puoi usare variabili, prova a digitare {{variable}}.', noDataPlaceHolder: 'Iniziare la conversazione con l\'utente può aiutare l\'IA a stabilire un legame più stretto con loro nelle applicazioni conversazionali.', varTip: 'Puoi usare variabili, prova a scrivere {{variable}}', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index 2bd5069b6c..a08714df71 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -99,6 +99,7 @@ const translation = { agentUserDescription: 'Un agente intelligente in grado di ragionare in modo iterativo e di utilizzare autonomamente gli strumenti per raggiungere gli obiettivi del compito.', advancedShortDescription: 'Flusso di lavoro migliorato per conversazioni multiple', chooseAppType: 'Scegli un tipo di app', + dropDSLToCreateApp: 'Trascina il file DSL qui per creare l\'app', }, editApp: 'Modifica Info', editAppTitle: 'Modifica Info App', @@ -144,6 +145,14 @@ const translation = { notConfigured: 'Configura il provider per abilitare il tracciamento', moreProvider: 'Altri Provider', }, + arize: { + title: 'Arize', + description: 'Osservabilità LLM di livello aziendale, valutazione online e offline, monitoraggio e sperimentazione—alimentata da OpenTelemetry. Progettata appositamente per applicazioni basate su LLM e agenti.', + }, + phoenix: { + title: 'Phoenix', + description: 'Piattaforma open-source basata su OpenTelemetry per osservabilità, valutazione, ingegneria dei prompt e sperimentazione per i tuoi flussi di lavoro e agenti LLM.', + }, langsmith: { title: 'LangSmith', description: @@ -175,6 +184,7 @@ const translation = { title: 'Intrecciare', description: 'Weave è una piattaforma open-source per valutare, testare e monitorare le applicazioni LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa', diff --git a/web/i18n/it-IT/common.ts b/web/i18n/it-IT/common.ts index b47bb23854..bdf8da1a98 100644 --- a/web/i18n/it-IT/common.ts +++ b/web/i18n/it-IT/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'Download non riuscito. Per favore riprova più tardi.', more: 'Di più', format: 'Formato', + selectAll: 'Seleziona tutto', + deSelectAll: 'Deseleziona tutto', }, errorMsg: { fieldRequired: '{{field}} è obbligatorio', @@ -495,7 +497,6 @@ const translation = { title: 'Le estensioni API forniscono una gestione centralizzata delle API, semplificando la configurazione per un facile utilizzo nelle applicazioni di Dify.', link: 'Scopri come sviluppare la tua estensione API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Aggiungi Estensione API', selector: { title: 'Estensione API', diff --git a/web/i18n/it-IT/dataset-creation.ts b/web/i18n/it-IT/dataset-creation.ts index ec9b5e3138..18df6a505f 100644 --- a/web/i18n/it-IT/dataset-creation.ts +++ b/web/i18n/it-IT/dataset-creation.ts @@ -66,8 +66,6 @@ const translation = { run: 'Esegui', firecrawlTitle: 'Estrai contenuti web con 🔥Firecrawl', firecrawlDoc: 'Documenti Firecrawl', - firecrawlDocLink: - 'https://docs.dify.ai/guides/knowledge-base/sync_from_website', options: 'Opzioni', crawlSubPage: 'Crawl sotto-pagine', limit: 'Limite', @@ -101,7 +99,6 @@ const translation = { configureJinaReader: 'Configura Jina Reader', configureWatercrawl: 'Configura Watercrawl', waterCrawlNotConfigured: 'Watercrawl non è configurato', - watercrawlDocLink: 'https://docs.dify.ai/it/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', }, cancel: 'Annulla', }, diff --git a/web/i18n/it-IT/dataset-documents.ts b/web/i18n/it-IT/dataset-documents.ts index b9afb1ea75..1651719a27 100644 --- a/web/i18n/it-IT/dataset-documents.ts +++ b/web/i18n/it-IT/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'Elimina', enableWarning: 'Il file archiviato non può essere abilitato', sync: 'Sincronizza', + resume: 'Riassumere', + pause: 'Pausa', }, index: { enable: 'Abilita', @@ -391,6 +393,8 @@ const translation = { regenerationSuccessMessage: 'È possibile chiudere questa finestra.', childChunkAdded: '1 blocco figlio aggiunto', childChunks_other: 'BLOCCHI FIGLIO', + keywordEmpty: 'La parola chiave non può essere vuota', + keywordDuplicate: 'La parola chiave esiste già', }, } diff --git a/web/i18n/it-IT/dataset.ts b/web/i18n/it-IT/dataset.ts index c2c4963371..9f66ee8e43 100644 --- a/web/i18n/it-IT/dataset.ts +++ b/web/i18n/it-IT/dataset.ts @@ -186,6 +186,7 @@ const translation = { checkName: { invalid: 'Il nome dei metadati può contenere solo lettere minuscole, numeri e underscore e deve iniziare con una lettera minuscola.', empty: 'Il nome dei metadati non può essere vuoto', + tooLong: 'Il nome dei metadati non può superare {{max}} caratteri.', }, batchEditMetadata: { multipleValue: 'Valore Multiplo', diff --git a/web/i18n/it-IT/plugin.ts b/web/i18n/it-IT/plugin.ts index 1c84b61fda..5f2a6d9dc7 100644 --- a/web/i18n/it-IT/plugin.ts +++ b/web/i18n/it-IT/plugin.ts @@ -137,6 +137,7 @@ const translation = { installing: 'Installazione...', install: 'Installare', readyToInstallPackages: 'Sto per installare i seguenti plugin {{num}}', + installWarning: 'Questo plugin non è consentito essere installato.', }, installFromGitHub: { installedSuccessfully: 'Installazione riuscita', @@ -178,7 +179,7 @@ const translation = { pluginsResult: '{{num}} risultati', noPluginFound: 'Nessun plug-in trovato', empower: 'Potenzia lo sviluppo dell\'intelligenza artificiale', - sortBy: 'Città nera', + sortBy: 'Ordina per', and: 'e', viewMore: 'Vedi di più', verifiedTip: 'Verificato da Dify', @@ -203,7 +204,6 @@ const translation = { install: '{{num}} installazioni', findMoreInMarketplace: 'Scopri di più su Marketplace', installPlugin: 'Installa il plugin', - submitPlugin: 'Invia plugin', searchPlugins: 'Plugin di ricerca', search: 'Ricerca', installFrom: 'INSTALLA DA', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'L\'attuale versione di Dify non è compatibile con questo plugin, si prega di aggiornare alla versione minima richiesta: {{minimalDifyVersion}}', requestAPlugin: 'Richiedi un plugin', + publishPlugins: 'Pubblicare plugin', } export default translation diff --git a/web/i18n/it-IT/tools.ts b/web/i18n/it-IT/tools.ts index 3c89d3a749..8aa119b45a 100644 --- a/web/i18n/it-IT/tools.ts +++ b/web/i18n/it-IT/tools.ts @@ -29,10 +29,21 @@ const translation = { add: 'aggiungi', added: 'aggiunto', manageInTools: 'Gestisci in Strumenti', - emptyTitle: 'Nessun strumento di flusso di lavoro disponibile', - emptyTip: 'Vai a `Flusso di lavoro -> Pubblica come Strumento`', - emptyTitleCustom: 'Nessun attrezzo personalizzato disponibile', - emptyTipCustom: 'Creare uno strumento personalizzato', + custom: { + title: 'Nessuno strumento personalizzato disponibile', + tip: 'Crea uno strumento personalizzato', + }, + workflow: { + title: 'Nessuno strumento workflow disponibile', + tip: 'Pubblica i workflow come strumenti nello Studio', + }, + mcp: { + title: 'Nessuno strumento MCP disponibile', + tip: 'Aggiungi un server MCP', + }, + agent: { + title: 'Nessuna strategia agente disponibile', + }, }, createTool: { title: 'Crea Strumento Personalizzato', @@ -162,6 +173,71 @@ const translation = { 'Nome chiamata strumento per il ragionamento e il prompting dell\'agente', noTools: 'Nessun utensile trovato', copyToolName: 'Copia nome', + mcp: { + create: { + cardTitle: 'Aggiungi Server MCP (HTTP)', + cardLink: 'Scopri di più sull\'integrazione del server MCP', + }, + noConfigured: 'Server Non Configurato', + updateTime: 'Aggiornato', + toolsCount: '{count} strumenti', + noTools: 'Nessuno strumento disponibile', + modal: { + title: 'Aggiungi Server MCP (HTTP)', + editTitle: 'Modifica Server MCP (HTTP)', + name: 'Nome & Icona', + namePlaceholder: 'Dai un nome al tuo server MCP', + serverUrl: 'URL del Server', + serverUrlPlaceholder: 'URL dell\'endpoint del server', + serverUrlWarning: 'L\'aggiornamento dell\'indirizzo del server può interrompere le applicazioni che dipendono da questo server', + serverIdentifier: 'Identificatore del Server', + serverIdentifierTip: 'Identificatore unico per il server MCP all\'interno dello spazio di lavoro. Solo lettere minuscole, numeri, underscore e trattini. Fino a 24 caratteri.', + serverIdentifierPlaceholder: 'Identificatore unico, es. mio-server-mcp', + serverIdentifierWarning: 'Il server non sarà riconosciuto dalle app esistenti dopo una modifica dell\'ID', + cancel: 'Annulla', + save: 'Salva', + confirm: 'Aggiungi & Autorizza', + }, + delete: 'Rimuovi Server MCP', + deleteConfirmTitle: 'Vuoi rimuovere {mcp}?', + operation: { + edit: 'Modifica', + remove: 'Rimuovi', + }, + authorize: 'Autorizza', + authorizing: 'Autorizzando...', + authorizingRequired: 'Autorizzazione richiesta', + authorizeTip: 'Dopo l\'autorizzazione, gli strumenti verranno visualizzati qui.', + update: 'Aggiorna', + updating: 'Aggiornamento in corso', + gettingTools: 'Ottimizzando Strumenti...', + updateTools: 'Aggiornando Strumenti...', + toolsEmpty: 'Strumenti non caricati', + getTools: 'Ottieni strumenti', + toolUpdateConfirmTitle: 'Aggiorna Lista Strumenti', + toolUpdateConfirmContent: 'L\'aggiornamento della lista degli strumenti può influire sulle app esistenti. Vuoi procedere?', + toolsNum: '{count} strumenti inclusi', + onlyTool: '1 strumento incluso', + identifier: 'Identificatore del Server (Fai clic per Copiare)', + server: { + title: 'Server MCP', + url: 'URL del Server', + reGen: 'Vuoi rigenerare l\'URL del server?', + addDescription: 'Aggiungi descrizione', + edit: 'Modifica descrizione', + modal: { + addTitle: 'Aggiungi descrizione per abilitare il server MCP', + editTitle: 'Modifica descrizione', + description: 'Descrizione', + descriptionPlaceholder: 'Spiega cosa fa questo strumento e come dovrebbe essere utilizzato dal LLM', + parameters: 'Parametri', + parametersTip: 'Aggiungi descrizioni per ogni parametro per aiutare il LLM a comprendere il loro scopo e le loro restrizioni.', + parametersPlaceholder: 'Scopo e restrizioni del parametro', + confirm: 'Abilitare Server MCP', + }, + publishTip: 'App non pubblicata. Pubblica l\'app prima.', + }, + }, } export default translation diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 26c790f4a0..d7e85dcc19 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Imposta variabile', needConnectTip: 'Questo passaggio non è collegato a nulla', maxTreeDepth: 'Limite massimo di {{depth}} nodi per ramo', - needEndNode: 'Deve essere aggiunto il blocco di Fine', - needAnswerNode: 'Deve essere aggiunto il blocco di Risposta', workflowProcess: 'Processo di flusso di lavoro', notRunning: 'Non ancora in esecuzione', previewPlaceholder: @@ -60,7 +58,6 @@ const translation = { learnMore: 'Scopri di più', copy: 'Copia', duplicate: 'Duplica', - addBlock: 'Aggiungi Blocco', pasteHere: 'Incolla Qui', pointerMode: 'Modalità Puntatore', handMode: 'Modalità Mano', @@ -119,6 +116,9 @@ const translation = { exportJPEG: 'Esporta come JPEG', noExist: 'Nessuna variabile del genere', exportPNG: 'Esporta come PNG', + needEndNode: 'Deve essere aggiunto il nodo finale', + addBlock: 'Aggiungi nodo', + needAnswerNode: 'Deve essere aggiunto il nodo di risposta', }, env: { envPanelTitle: 'Variabili d\'Ambiente', @@ -133,6 +133,8 @@ const translation = { value: 'Valore', valuePlaceholder: 'valore env', secretTip: 'Utilizzato per definire informazioni o dati sensibili, con impostazioni DSL configurate per la prevenzione delle fughe.', + description: 'Descrizione', + descriptionPlaceholder: 'Descrivi la variabile', }, export: { title: 'Esportare variabili d\'ambiente segrete?', @@ -181,19 +183,19 @@ const translation = { stepForward_other: '{{count}} passi avanti', sessionStart: 'Inizio sessione', currentState: 'Stato attuale', - nodeTitleChange: 'Titolo del blocco modificato', - nodeDescriptionChange: 'Descrizione del blocco modificata', - nodeDragStop: 'Blocco spostato', - nodeChange: 'Blocco modificato', - nodeConnect: 'Blocco collegato', - nodePaste: 'Blocco incollato', - nodeDelete: 'Blocco eliminato', - nodeAdd: 'Blocco aggiunto', - nodeResize: 'Blocco ridimensionato', noteAdd: 'Nota aggiunta', noteChange: 'Nota modificata', noteDelete: 'Nota eliminata', - edgeDelete: 'Blocco scollegato', + nodeDescriptionChange: 'Descrizione del nodo cambiata', + nodePaste: 'Nodo incollato', + nodeChange: 'Nodo cambiato', + nodeResize: 'Nodo ridimensionato', + nodeDelete: 'Nodo eliminato', + nodeTitleChange: 'Titolo del nodo cambiato', + edgeDelete: 'Nodo disconnesso', + nodeAdd: 'Nodo aggiunto', + nodeDragStop: 'Nodo spostato', + nodeConnect: 'Nodo connesso', }, errorMsg: { fieldRequired: '{{field}} è richiesto', @@ -222,8 +224,6 @@ const translation = { loop: 'Anello', }, tabs: { - 'searchBlock': 'Cerca blocco', - 'blocks': 'Blocchi', 'tools': 'Strumenti', 'allTool': 'Tutti', 'builtInTool': 'Integrato', @@ -237,6 +237,8 @@ const translation = { 'searchTool': 'Strumento di ricerca', 'agent': 'Strategia dell\'agente', 'plugin': 'Plugin', + 'searchBlock': 'Cerca nodo', + 'blocks': 'Nodi', }, blocks: { 'start': 'Inizio', @@ -302,22 +304,24 @@ const translation = { }, panel: { userInputField: 'Campo di Input Utente', - changeBlock: 'Cambia Blocco', helpLink: 'Link di Aiuto', about: 'Informazioni', createdBy: 'Creato da ', nextStep: 'Prossimo Passo', - addNextStep: 'Aggiungi il prossimo blocco in questo flusso di lavoro', - selectNextStep: 'Seleziona Prossimo Blocco', runThisStep: 'Esegui questo passo', checklist: 'Checklist', checklistTip: 'Assicurati che tutti i problemi siano risolti prima di pubblicare', checklistResolved: 'Tutti i problemi sono risolti', - organizeBlocks: 'Organizza blocchi', change: 'Cambia', optional: '(opzionale)', moveToThisNode: 'Sposta a questo nodo', + changeBlock: 'Cambia Nodo', + selectNextStep: 'Seleziona il prossimo passo', + organizeBlocks: 'Organizzare i nodi', + addNextStep: 'Aggiungi il prossimo passo in questo flusso di lavoro', + minimize: 'Esci dalla modalità schermo intero', + maximize: 'Massimizza Canvas', }, nodes: { common: { @@ -375,6 +379,7 @@ const translation = { retryFailed: 'Nuovo tentativo non riuscito', ms: 'ms', }, + typeSwitch: {}, }, start: { required: 'richiesto', @@ -551,6 +556,10 @@ const translation = { placeholder: 'Incolla qui la stringa cURL', title: 'Importazione da cURL', }, + verifySSL: { + title: 'Verifica il certificato SSL', + warningTooltip: 'Disabilitare la verifica SSL non è raccomandato per gli ambienti di produzione. Questo dovrebbe essere utilizzato solo in sviluppo o test, poiché rende la connessione vulnerabile a minacce alla sicurezza come gli attacchi man-in-the-middle.', + }, }, code: { inputVars: 'Variabili di Input', @@ -686,6 +695,7 @@ const translation = { inputVars: 'Variabili di Input', outputVars: { className: 'Nome Classe', + usage: 'Informazioni sull\'utilizzo del modello', }, class: 'Classe', classNamePlaceholder: 'Scrivi il nome della tua classe', @@ -700,6 +710,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Variabile di Input', + outputVars: { + isSuccess: 'È successo. In caso di successo il valore è 1, in caso di fallimento il valore è 0.', + errorReason: 'Motivo dell\'errore', + usage: 'Informazioni sull\'utilizzo del modello', + }, extractParameters: 'Estrai Parametri', importFromTool: 'Importa dagli strumenti', addExtractParameter: 'Aggiungi Parametro Estratto', @@ -722,9 +737,6 @@ const translation = { reasoningMode: 'Modalità di ragionamento', reasoningModeTip: 'Puoi scegliere la modalità di ragionamento appropriata in base alla capacità del modello di rispondere alle istruzioni per la chiamata delle funzioni o i prompt.', - isSuccess: - 'È successo. In caso di successo il valore è 1, in caso di fallimento il valore è 0.', - errorReason: 'Motivo dell\'errore', }, iteration: { deleteTitle: 'Eliminare Nodo Iterazione?', @@ -942,6 +954,35 @@ const translation = { restorationTip: 'Dopo il ripristino della versione, la bozza attuale verrà sovrascritta.', title: 'Versioni', }, + debug: { + noData: { + runThisNode: 'Esegui questo nodo', + description: 'I risultati dell\'ultima esecuzione verranno visualizzati qui', + }, + variableInspect: { + trigger: { + cached: 'Visualizza le variabili memorizzate nella cache', + clear: 'Chiaro', + running: 'Caching stato di esecuzione', + normal: 'Ispezione Variabile', + stop: 'Ferma la corsa', + }, + chatNode: 'Conversazione', + clearNode: 'Svuota la variabile cached', + envNode: 'Ambiente', + systemNode: 'Sistema', + title: 'Ispezione delle variabili', + edited: 'Modificato', + emptyLink: 'Scopri di più', + resetConversationVar: 'Reimposta la variabile della conversazione al valore predefinito', + view: 'Visualizza log', + clearAll: 'Ripristina tutto', + reset: 'Ripristina il valore dell\'ultima esecuzione', + emptyTip: 'Dopo aver eseguito un nodo sulla tela o eseguendo un nodo passo dopo passo, puoi visualizzare il valore attuale della variabile nodo in Ispeziona Variabile.', + }, + settingsTab: 'Impostazioni', + lastRunTab: 'Ultima corsa', + }, } export default translation diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index f862f3f2f7..decbe4863e 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -434,6 +434,7 @@ const translation = { writeOpener: 'オープナーを書く', placeholder: 'ここにオープナーメッセージを書いてください。変数を使用できます。{{variable}} を入力してみてください。', openingQuestion: '開始質問', + openingQuestionPlaceholder: '変数を使用できます。{{variable}} と入力してみてください。', noDataPlaceHolder: 'ユーザーとの会話を開始すると、会話アプリケーションで彼らとのより密接な関係を築くのに役立ちます。', varTip: '変数を使用できます。{{variable}} を入力してみてください', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index b501bc129e..2058b29d9e 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -100,6 +100,7 @@ const translation = { chatbotUserDescription: '簡単な設定で LLM ベースのチャットボットを迅速に構築します。Chatflow は後で切り替えることができます。', workflowUserDescription: 'ドラッグ&ドロップの簡易性で自律型 AI ワークフローを視覚的に構築', completionUserDescription: '簡単な構成でテキスト生成タスク用の AI アシスタントをすばやく構築します。', + dropDSLToCreateApp: 'アプリを作成するにはここにDSLファイルをドロップしてください', }, editApp: '情報を編集する', editAppTitle: 'アプリ情報を編集する', @@ -144,6 +145,14 @@ const translation = { notConfigured: 'トレース機能を有効化するためには、サービスの設定が必要です。', moreProvider: 'その他のプロバイダー', }, + arize: { + title: 'Arize', + description: 'エンタープライズグレードのLLM可観測性、オンラインおよびオフライン評価、モニタリング、実験—OpenTelemetryによって支えられています。LLMおよびエージェント駆動型アプリケーション向けに特別に設計されています。', + }, + phoenix: { + title: 'Phoenix', + description: 'オープンソースおよびOpenTelemetryベースの可観測性、評価、プロンプトエンジニアリング、実験プラットフォームで、LLMワークフローおよびエージェントに対応します。', + }, langsmith: { title: 'LangSmith', description: 'LLM を利用したアプリケーションのライフサイクル全段階を支援する、オールインワンの開発者向けプラットフォームです。', @@ -171,6 +180,7 @@ const translation = { title: '織る', description: 'Weave は、LLM アプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。', }, + aliyun: {}, }, answerIcon: { title: 'Web アプリアイコンを使用して🤖を置き換える', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 85f5863761..0c33c83847 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -64,6 +64,8 @@ const translation = { in: '中', format: 'フォーマット', more: 'もっと', + selectAll: 'すべて選択', + deSelectAll: 'すべて選択解除', }, errorMsg: { fieldRequired: '{{field}}は必要です', @@ -485,7 +487,6 @@ const translation = { apiBasedExtension: { title: 'API 拡張機能は、Dify のアプリケーション全体での簡単な使用のための設定を簡素化し、集中的な API 管理を提供します。', link: '独自の API 拡張機能を開発する方法について学ぶ。', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'API 拡張機能を追加', selector: { title: 'API 拡張機能', diff --git a/web/i18n/ja-JP/dataset-creation.ts b/web/i18n/ja-JP/dataset-creation.ts index 15bee57c0b..262f0cf8d9 100644 --- a/web/i18n/ja-JP/dataset-creation.ts +++ b/web/i18n/ja-JP/dataset-creation.ts @@ -72,7 +72,6 @@ const translation = { run: '実行', firecrawlTitle: '🔥Firecrawl を使っでウエブコンテンツを抽出', firecrawlDoc: 'Firecrawl ドキュメント', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', jinaReaderTitle: 'サイト全体を Markdown に変換する', jinaReaderDoc: 'Jina Reader の詳細', jinaReaderDocLink: 'https://jina.ai/reader', @@ -98,7 +97,6 @@ const translation = { watercrawlDoc: 'ウォータークローリングの文書', watercrawlTitle: 'Watercrawl を使用してウェブコンテンツを抽出する', waterCrawlNotConfigured: 'Watercrawl は設定されていません', - watercrawlDocLink: 'https://docs.dify.ai/ja/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', }, }, stepTwo: { diff --git a/web/i18n/ja-JP/dataset-documents.ts b/web/i18n/ja-JP/dataset-documents.ts index ecdbbf512c..ac7a94fafc 100644 --- a/web/i18n/ja-JP/dataset-documents.ts +++ b/web/i18n/ja-JP/dataset-documents.ts @@ -30,6 +30,8 @@ const translation = { delete: '削除', enableWarning: 'アーカイブされたファイルは有効にできません', sync: '同期', + pause: '一時停止', + resume: '再開', }, index: { enable: '有効にする', @@ -388,6 +390,8 @@ const translation = { editedAt: '編集日時', expandChunks: 'チャンクを展開', collapseChunks: 'チャンクを折りたたむ', + keywordDuplicate: 'そのキーワードは既に存在しています', + keywordEmpty: 'キーワードは空であってはいけません', }, } diff --git a/web/i18n/ja-JP/dataset.ts b/web/i18n/ja-JP/dataset.ts index 2bdc4a8d28..1a40f17afc 100644 --- a/web/i18n/ja-JP/dataset.ts +++ b/web/i18n/ja-JP/dataset.ts @@ -183,6 +183,7 @@ const translation = { checkName: { empty: 'メタデータ名を入力してください', invalid: 'メタデータ名は小文字、数字、アンダースコアのみを使用し、小文字で始める必要があります', + tooLong: 'メタデータ名は {{max}} 文字を超えることはできません', }, batchEditMetadata: { editMetadata: 'メタデータを編集', diff --git a/web/i18n/ja-JP/education.ts b/web/i18n/ja-JP/education.ts index 4fae5ac28e..20544b1dee 100644 --- a/web/i18n/ja-JP/education.ts +++ b/web/i18n/ja-JP/education.ts @@ -2,7 +2,7 @@ const translation = { toVerified: '教育認証を取得', toVerifiedTip: { front: '現在、教育認証ステータスを取得する資格があります。以下に教育情報を入力し、認証プロセスを完了すると、Dify プロフェッショナルプランの', - coupon: '50%割引クーポン', + coupon: '100%割引クーポン', end: 'を受け取ることができます。', }, currentSigned: '現在ログイン中のアカウントは', @@ -38,9 +38,9 @@ const translation = { submitError: 'フォームの送信に失敗しました。しばらくしてから再度ご提出ください。', learn: '教育認証の取得方法はこちら', successTitle: 'Dify 教育認証を取得しました!', - successContent: 'お客様のアカウントに Dify プロフェッショナルプランの 50% 割引クーポン を発行しました。有効期間は 1 年間 ですので、期限内にご利用ください。', + successContent: 'お客様のアカウントに Dify プロフェッショナルプランの 100% 割引クーポン を発行しました。有効期間は 1 年間 ですので、期限内にご利用ください。', rejectTitle: 'Dify 教育認証が拒否されました', - rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 50%割引クーポン を受け取ることはできません。', + rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 100%割引クーポン を受け取ることはできません。', emailLabel: '現在のメールアドレス', } diff --git a/web/i18n/ja-JP/plugin.ts b/web/i18n/ja-JP/plugin.ts index bc976074ea..b886c4955c 100644 --- a/web/i18n/ja-JP/plugin.ts +++ b/web/i18n/ja-JP/plugin.ts @@ -137,6 +137,7 @@ const translation = { installPlugin: 'プラグインをインストールする', back: '戻る', uploadingPackage: '{{packageName}}をアップロード中...', + installWarning: 'このプラグインはインストールを許可されていません。', }, installFromGitHub: { installedSuccessfully: 'インストールに成功しました', @@ -206,12 +207,12 @@ const translation = { searchTools: '検索ツール...', installPlugin: 'プラグインをインストールする', searchInMarketplace: 'マーケットプレイスで検索', - submitPlugin: 'プラグインを提出する', difyVersionNotCompatible: '現在の Dify バージョンはこのプラグインと互換性がありません。最小バージョンは{{minimalDifyVersion}}です。', metadata: { title: 'プラグイン', }, requestAPlugin: 'プラグインをリクエストする', + publishPlugins: 'プラグインを公開する', } export default translation diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index cf9dad95b3..f96a5f4182 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '追加', added: '追加済', manageInTools: 'ツールリストに移動して管理する', - emptyTitle: '利用可能なワークフローツールはありません', - emptyTip: '追加するには、「ワークフロー -> ツールとして公開」に移動する', - emptyTitleCustom: 'カスタムツールはありません', - emptyTipCustom: 'カスタムツールの作成', + custom: { + title: 'カスタムツールはありません', + tip: 'カスタムツールを作成する', + }, + workflow: { + title: '利用可能なワークフローツールはありません', + tip: 'スタジオでワークフローをツールに公開する', + }, + mcp: { + title: '利用可能なMCPツールはありません', + tip: 'MCPサーバーを追加する', + }, + agent: { + title: 'Agent strategy は利用できません', + }, }, createTool: { title: 'カスタムツールを作成する', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'ツール呼び出し名、エージェントの推論とプロンプトの単語に使用されます', copyToolName: '名前をコピー', noTools: 'ツールが見つかりませんでした', + mcp: { + create: { + cardTitle: 'MCPサーバー(HTTP)を追加', + cardLink: 'MCPサーバー統合について詳しく知る', + }, + noConfigured: '未設定', + updateTime: '更新日時', + toolsCount: '{{count}} 個のツール', + noTools: '利用可能なツールはありません', + modal: { + title: 'MCPサーバー(HTTP)を追加', + editTitle: 'MCPサーバー(HTTP)を編集', + name: '名前とアイコン', + namePlaceholder: 'MCPサーバーの名前を入力', + serverUrl: 'サーバーURL', + serverUrlPlaceholder: 'サーバーエンドポイントのURLを入力', + serverUrlWarning: 'サーバーアドレスを更新すると、このサーバーに依存するアプリケーションに影響を与える可能性があります。', + serverIdentifier: 'サーバー識別子', + serverIdentifierTip: 'ワークスペース内でのMCPサーバーのユニーク識別子です。使用可能な文字は小文字、数字、アンダースコア、ハイフンで、最大24文字です。', + serverIdentifierPlaceholder: 'ユニーク識別子(例:my-mcp-server)', + serverIdentifierWarning: 'IDを変更すると、既存のアプリケーションではサーバーが認識できなくなります。', + cancel: 'キャンセル', + save: '保存', + confirm: '追加して承認', + }, + delete: 'MCPサーバーを削除', + deleteConfirmTitle: '{{mcp}} を削除しますか?', + operation: { + edit: '編集', + remove: '削除', + }, + authorize: '承認', + authorizing: '承認中...', + authorizingRequired: '承認が必要です。', + authorizeTip: '承認後、このページにツールが表示されるようになります。', + update: '更新', + updating: '更新中...', + gettingTools: 'ツール取得中...', + updateTools: 'ツール更新中...', + toolsEmpty: 'ツールが読み込まれていません', + getTools: 'ツールを取得', + toolUpdateConfirmTitle: 'ツールリストの更新', + toolUpdateConfirmContent: 'ツールリストを更新すると、既存のアプリケーションに重大な影響を与える可能性があります。続行しますか?', + toolsNum: '{{count}} 個のツールが含まれています', + onlyTool: '1つのツールが含まれています', + identifier: 'サーバー識別子(クリックしてコピー)', + server: { + title: 'MCPサーバー', + url: 'サーバーURL', + reGen: 'サーバーURLを再生成しますか?', + addDescription: '説明を追加', + edit: '説明を編集', + modal: { + addTitle: 'MCPサーバーを有効化するための説明を追加', + editTitle: '説明を編集', + description: '説明', + descriptionPlaceholder: 'このツールの機能とLLM(大規模言語モデル)での使用方法を説明してください。', + parameters: 'パラメータ', + parametersTip: '各パラメータの説明を追加して、LLMがその目的と制約を理解できるようにします。', + parametersPlaceholder: 'パラメータの目的と制約', + confirm: 'MCPサーバーを有効にする', + }, + publishTip: 'アプリが公開されていません。まずアプリを公開してください。', + }, + }, } export default translation diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 1320e7f3b6..04702194f8 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -127,6 +127,8 @@ const translation = { value: '値', valuePlaceholder: '変数値を入力', secretTip: 'この変数は機密情報やデータを定義するために使用されます。DSL をエクスポートするときに漏洩防止メカニズムを設定されます。', + description: '説明', + descriptionPlaceholder: '変数の説明を入力', }, export: { title: 'シークレット環境変数をエクスポートしますか?', @@ -308,6 +310,8 @@ const translation = { change: '変更', optional: '(任意)', moveToThisNode: 'このノードに移動する', + maximize: 'キャンバスを最大化する', + minimize: '全画面を終了する', }, nodes: { common: { @@ -365,6 +369,7 @@ const translation = { ms: 'ミリ秒', retries: '再試行回数:{{num}}', }, + typeSwitch: {}, }, start: { required: '必須', @@ -541,6 +546,10 @@ const translation = { title: 'cURL からインポート', placeholder: 'ここに cURL 文字列を貼り付けます', }, + verifySSL: { + title: 'SSL証明書を確認する', + warningTooltip: 'SSL検証を無効にすることは、本番環境では推奨されません。これは開発またはテストのみに使用すべきであり、中間者攻撃などのセキュリティ脅威に対して接続を脆弱にするためです。', + }, }, code: { inputVars: '入力変数', @@ -548,6 +557,7 @@ const translation = { advancedDependencies: '高度な依存関係', advancedDependenciesTip: '消費に時間がかかる、またはデフォルトで組み込まれていない事前ロードされた依存関係を追加します', searchDependencies: '依存関係を検索', + syncFunctionSignature: 'コードの関数署名を同期', }, templateTransform: { inputVars: '入力変数', @@ -673,6 +683,7 @@ const translation = { inputVars: '入力変数', outputVars: { className: 'クラス名', + usage: 'モデル使用量', }, class: 'クラス', classNamePlaceholder: 'クラス名を入力してください', @@ -686,6 +697,11 @@ const translation = { }, parameterExtractor: { inputVar: '入力変数', + outputVars: { + isSuccess: '成功。成功した場合の値は 1、失敗した場合の値は 0 です。', + errorReason: 'エラーの理由', + usage: 'モデル使用量', + }, extractParameters: 'パラメーターを抽出', importFromTool: 'ツールからインポート', addExtractParameter: '抽出パラメーターを追加', @@ -705,8 +721,6 @@ const translation = { advancedSetting: '高度な設定', reasoningMode: '推論モード', reasoningModeTip: '関数呼び出しやプロンプトの指示に応答するモデルの能力に基づいて、適切な推論モードを選択できます。', - isSuccess: '成功。成功した場合の値は 1、失敗した場合の値は 0 です。', - errorReason: 'エラーの理由', }, iteration: { deleteTitle: 'イテレーションノードを削除しますか?', @@ -916,6 +930,35 @@ const translation = { updateFailure: '更新に失敗しました', }, }, + debug: { + noData: { + runThisNode: 'このノードを実行してください', + description: '最後の実行の結果がここに表示されます', + }, + variableInspect: { + trigger: { + clear: 'クリア', + running: 'キャッシング実行状況', + cached: 'キャッシュされた変数を表示', + stop: '走るのを止めて', + normal: '変数検査', + }, + clearAll: 'すべてリセット', + emptyLink: 'もっと学ぶ', + systemNode: 'システム', + view: 'ログを表示', + resetConversationVar: '会話の変数をデフォルト値にリセットする', + chatNode: '会話', + reset: '最後の実行値にリセットする', + clearNode: 'キャッシュされた変数をクリアする', + edited: '編集された', + title: '変数検査', + envNode: '環境', + emptyTip: 'キャンバス上でノードをステップ実行するか、ノードを一歩ずつ実行した後、変数インスペクトでノード変数の現在の値を確認できます。', + }, + settingsTab: '設定', + lastRunTab: '最後の実行', + }, } export default translation diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index 3c5a3f4b1f..b84946841f 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: '오프너 작성', placeholder: '여기에 오프너 메시지를 작성하세요. 변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', openingQuestion: '시작 질문', + openingQuestionPlaceholder: '변수를 사용할 수 있습니다. {{variable}}을(를) 입력해 보세요.', noDataPlaceHolder: '사용자와의 대화를 시작하면 대화 애플리케이션에서 그들과 더 밀접한 관계를 구축하는 데 도움이 됩니다.', varTip: '변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', tooShort: '대화 시작에는 최소 20 단어의 초기 프롬프트가 필요합니다.', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 7227fd3171..96407f829b 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -16,7 +16,8 @@ const translation = { importDSL: 'DSL 파일 가져오기', createFromConfigFile: 'DSL 파일에서 생성하기', deleteAppConfirmTitle: '이 앱을 삭제하시겠습니까?', - deleteAppConfirmContent: '앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.', + deleteAppConfirmContent: + '앱을 삭제하면 복구할 수 없습니다. 사용자는 더 이상 앱에 액세스할 수 없으며 모든 프롬프트 설정 및 로그가 영구적으로 삭제됩니다.', appDeleted: '앱이 삭제되었습니다', appDeleteFailed: '앱 삭제 실패', join: '커뮤니티에 참여하기', @@ -26,20 +27,25 @@ const translation = { startFromBlank: '빈 상태로 시작', startFromTemplate: '템플릿에서 시작', captionAppType: '어떤 종류의 앱을 만들어 보시겠어요?', - chatbotDescription: '대화형 어플리케이션을 만듭니다. 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', - completionDescription: '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만듭니다. 기사, 요약, 번역 등을 생성할 수 있습니다.', + chatbotDescription: + '대화형 어플리케이션을 만듭니다. 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', + completionDescription: + '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만듭니다. 기사, 요약, 번역 등을 생성할 수 있습니다.', completionWarning: '이 종류의 앱은 더 이상 지원되지 않습니다.', agentDescription: '작업을 자동으로 완료하는 지능형 에이전트를 만듭니다.', - workflowDescription: '고도로 사용자 지정 가능한 워크플로우에 기반한 고품질 텍스트 생성 어플리케이션을 만듭니다. 경험 있는 사용자를 위한 것입니다.', + workflowDescription: + '고도로 사용자 지정 가능한 워크플로우에 기반한 고품질 텍스트 생성 어플리케이션을 만듭니다. 경험 있는 사용자를 위한 것입니다.', workflowWarning: '현재 베타 버전입니다.', chatbotType: '챗봇 오케스트레이션 방식', basic: '기본', basicTip: '초보자용. 나중에 Chatflow 로 전환할 수 있습니다.', basicFor: '초보자용', - basicDescription: '기본 오케스트레이션은 내장된 프롬프트를 수정할 수 없고 간단한 설정을 사용하여 챗봇 앱을 오케스트레이션합니다. 초보자용입니다.', + basicDescription: + '기본 오케스트레이션은 내장된 프롬프트를 수정할 수 없고 간단한 설정을 사용하여 챗봇 앱을 오케스트레이션합니다. 초보자용입니다.', advanced: 'Chatflow', advancedFor: '고급 사용자용', - advancedDescription: '워크플로우 오케스트레이션은 워크플로우 형식으로 챗봇을 오케스트레이션하며 내장된 프롬프트를 편집할 수 있는 고급 사용자 정의 기능을 제공합니다. 경험이 많은 사용자용입니다.', + advancedDescription: + '워크플로우 오케스트레이션은 워크플로우 형식으로 챗봇을 오케스트레이션하며 내장된 프롬프트를 편집할 수 있는 고급 사용자 정의 기능을 제공합니다. 경험이 많은 사용자용입니다.', captionName: '앱 아이콘과 이름', appNamePlaceholder: '앱 이름을 입력하세요', captionDescription: '설명', @@ -47,10 +53,12 @@ const translation = { useTemplate: '이 템플릿 사용', previewDemo: '데모 미리보기', chatApp: '어시스턴트', - chatAppIntro: '대화형 어플리케이션을 만들고 싶어요. 이 어플리케이션은 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', + chatAppIntro: + '대화형 어플리케이션을 만들고 싶어요. 이 어플리케이션은 질문과 답변 형식을 사용하여 다단계 대화를 지원합니다.', agentAssistant: '새로운 에이전트 어시스턴트', completeApp: '텍스트 생성기', - completeAppIntro: '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만들고 싶어요. 기사, 요약, 번역 등을 생성합니다.', + completeAppIntro: + '프롬프트를 기반으로 품질 높은 텍스트를 생성하는 어플리케이션을 만들고 싶어요. 기사, 요약, 번역 등을 생성합니다.', showTemplates: '템플릿 선택', hideTemplates: '모드 선택으로 돌아가기', Create: '만들기', @@ -66,13 +74,16 @@ const translation = { appCreateDSLErrorTitle: '버전 비호환성', appCreateDSLErrorPart2: '계속하시겠습니까?', appCreateDSLErrorPart3: '현재 응용 프로그램 DSL 버전:', - appCreateDSLWarning: '주의: DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', - appCreateDSLErrorPart1: 'DSL 버전에서 상당한 차이가 감지되었습니다. 강제로 가져오면 응용 프로그램이 오작동할 수 있습니다.', + appCreateDSLWarning: + '주의: DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', + appCreateDSLErrorPart1: + 'DSL 버전에서 상당한 차이가 감지되었습니다. 강제로 가져오면 응용 프로그램이 오작동할 수 있습니다.', chooseAppType: '앱 유형 선택', forBeginners: '초보자용 기본 앱 유형', forAdvanced: '고급 사용자용', chatbotShortDescription: '간단한 설정으로 LLM 기반 챗봇', - workflowUserDescription: '드래그 앤 드롭으로 자율 AI 워크플로우를 시각적으로 구축', + workflowUserDescription: + '드래그 앤 드롭으로 자율 AI 워크플로우를 시각적으로 구축', noTemplateFoundTip: '다른 키워드를 사용하여 검색해 보십시오.', noIdeaTip: '아이디어가 없으신가요? 템플릿을 확인해 보세요', optional: '선택적', @@ -80,15 +91,20 @@ const translation = { completionShortDescription: '텍스트 생성 작업을 위한 AI 도우미', learnMore: '더 알아보세요', foundResults: '{{개수}} 결과', - agentShortDescription: '추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트', + agentShortDescription: + '추론 및 자율적인 도구 사용 기능이 있는 지능형 에이전트', advancedShortDescription: '다중 대화를 위해 강화된 워크플로우', noAppsFound: '앱을 찾을 수 없습니다.', foundResult: '{{개수}} 결과', - completionUserDescription: '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', - chatbotUserDescription: '간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow 로 전환할 수 있습니다.', + completionUserDescription: + '간단한 구성으로 텍스트 생성 작업을 위한 AI 도우미를 빠르게 구축합니다.', + chatbotUserDescription: + '간단한 구성으로 LLM 기반 챗봇을 빠르게 구축할 수 있습니다. 나중에 Chatflow 로 전환할 수 있습니다.', workflowShortDescription: '지능형 자동화를 위한 에이전트 플로우', - agentUserDescription: '작업 목표를 달성하기 위해 반복적인 추론과 자율적인 도구를 사용할 수 있는 지능형 에이전트입니다.', + agentUserDescription: + '작업 목표를 달성하기 위해 반복적인 추론과 자율적인 도구를 사용할 수 있는 지능형 에이전트입니다.', advancedUserDescription: '메모리 기능과 챗봇 인터페이스를 갖춘 워크플로우', + dropDSLToCreateApp: '여기에 DSL 파일을 드롭하여 앱을 불러오세요.', }, editApp: '정보 편집하기', editAppTitle: '앱 정보 편집하기', @@ -101,7 +117,8 @@ const translation = { image: '이미지', }, switch: '워크플로우 오케스트레이션으로 전환하기', - switchTipStart: '새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ', + switchTipStart: + '새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ', switchTip: '전환을 허용하지 않습니다', switchTipEnd: ' 기본적인 오케스트레이션으로 되돌릴 수 없습니다.', switchLabel: '생성될 앱의 복사본', @@ -125,19 +142,32 @@ const translation = { disabled: '비활성화됨', disabledTip: '먼저 제공업체를 구성해 주세요', enabled: '서비스 중', - tracingDescription: 'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제 3 자 추적 플랫폼에 캡처합니다.', + tracingDescription: + 'LLM 호출, 컨텍스트, 프롬프트, HTTP 요청 등 앱 실행의 전체 컨텍스트를 제 3 자 추적 플랫폼에 캡처합니다.', configProviderTitle: { configured: '구성됨', notConfigured: '추적을 활성화하려면 제공업체를 구성하세요', moreProvider: '더 많은 제공업체', }, + arize: { + title: 'Arize', + description: + '엔터프라이즈급 LLM 가시성, 온라인 및 오프라인 평가, 모니터링 및 실험—OpenTelemetry를 기반으로 합니다. LLM 및 에이전트 기반 애플리케이션을 위해 특별히 설계되었습니다.', + }, + phoenix: { + title: 'Phoenix', + description: + '오픈소스 및 OpenTelemetry 기반의 가시성, 평가, 프롬프트 엔지니어링 및 실험 플랫폼으로, LLM 워크플로우 및 에이전트를 지원합니다.', + }, langsmith: { title: 'LangSmith', - description: 'LLM 기반 애플리케이션 수명 주기의 모든 단계를 위한 올인원 개발자 플랫폼.', + description: + 'LLM 기반 애플리케이션 수명 주기의 모든 단계를 위한 올인원 개발자 플랫폼.', }, langfuse: { title: 'Langfuse', - description: 'LLM 애플리케이션을 디버그하고 개선하기 위한 추적, 평가, 프롬프트 관리 및 메트릭.', + description: + 'LLM 애플리케이션을 디버그하고 개선하기 위한 추적, 평가, 프롬프트 관리 및 메트릭.', }, inUse: '사용 중', configProvider: { @@ -148,22 +178,28 @@ const translation = { secretKey: '비밀 키', viewDocsLink: '{{key}} 문서 보기', removeConfirmTitle: '{{key}} 구성을 제거하시겠습니까?', - removeConfirmContent: '현재 구성이 사용 중입니다. 제거하면 추적 기능이 꺼집니다.', + removeConfirmContent: + '현재 구성이 사용 중입니다. 제거하면 추적 기능이 꺼집니다.', }, view: '보기', opik: { title: '오픽', - description: 'Opik 은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.', + description: + 'Opik 은 LLM 애플리케이션을 평가, 테스트 및 모니터링하기 위한 오픈 소스 플랫폼입니다.', }, weave: { title: '직조하다', - description: 'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', + description: + 'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', }, + aliyun: {}, }, answerIcon: { - description: 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', + description: + 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', title: 'web app 아이콘을 사용하여 🤖', - descriptionInExplore: 'Explore 에서 web app 아이콘을 사용하여 바꿀🤖지 여부', + descriptionInExplore: + 'Explore 에서 web app 아이콘을 사용하여 바꿀🤖지 여부', }, importFromDSL: 'DSL 에서 가져오기', importFromDSLFile: 'DSL 파일에서', @@ -202,8 +238,10 @@ const translation = { structured: '구조화된', configure: '설정하다', moreFillTip: '최대 10 단계 중첩을 표시합니다.', - modelNotSupportedTip: '현재 모델은 이 기능을 지원하지 않으며 자동으로 프롬프트 주입으로 다운그레이드됩니다.', - structuredTip: '구조화된 출력은 모델이 제공한 JSON 스키마를 항상 준수하는 응답을 생성하도록 보장하는 기능입니다.', + modelNotSupportedTip: + '현재 모델은 이 기능을 지원하지 않으며 자동으로 프롬프트 주입으로 다운그레이드됩니다.', + structuredTip: + '구조화된 출력은 모델이 제공한 JSON 스키마를 항상 준수하는 응답을 생성하도록 보장하는 기능입니다.', }, accessItemsDescription: { anyone: '누구나 웹 앱에 접근할 수 있습니다.', @@ -231,7 +269,8 @@ const translation = { members_one: '{{count}} 회원', members_other: '{{count}} 회원', noGroupsOrMembers: '선택된 그룹 또는 멤버가 없습니다.', - webAppSSONotEnabledTip: '웹 앱 인증 방법을 구성하려면 엔터프라이즈 관리자인에게 문의하십시오.', + webAppSSONotEnabledTip: + '웹 앱 인증 방법을 구성하려면 엔터프라이즈 관리자인에게 문의하십시오.', updateSuccess: '업데이트가 성공적으로 완료되었습니다.', description: '웹 앱 접근 권한 설정', }, diff --git a/web/i18n/ko-KR/billing.ts b/web/i18n/ko-KR/billing.ts index 87ccf27fe0..fbb2609adc 100644 --- a/web/i18n/ko-KR/billing.ts +++ b/web/i18n/ko-KR/billing.ts @@ -9,7 +9,7 @@ const translation = { buyPermissionDeniedTip: '구독하려면 엔터프라이즈 관리자에게 문의하세요', plansCommon: { title: '당신에게 맞는 요금제를 선택하세요', - yearlyTip: '연간 구독 시 2 개월 무료!', + yearlyTip: '연간 구독 시 2개월 무료!', mostPopular: '가장 인기 있는', planRange: { monthly: '월간', @@ -20,21 +20,25 @@ const translation = { save: '절약 ', free: '무료', currentPlan: '현재 요금제', - contractSales: '영업에 문의하기', + contractSales: '영업팀에 문의하기', contractOwner: '팀 관리자에게 문의하기', startForFree: '무료로 시작하기', getStartedWith: '시작하기 ', - contactSales: '영업에 문의하기', - talkToSales: '영업과 상담하기', + contactSales: '영업팀에 문의하기', + talkToSales: '영업팀과 상담하기', modelProviders: '모델 제공자', teamMembers: '팀 멤버', buildApps: '앱 만들기', vectorSpace: '벡터 공간', - vectorSpaceBillingTooltip: '1MB 당 약 120 만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings 을 기반으로 추정되며 모델에 따라 다릅니다).', - vectorSpaceTooltip: '벡터 공간은 LLM 이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.', + vectorSpaceBillingTooltip: + '1MB 당 약 120 만 글자의 벡터화된 데이터를 저장할 수 있습니다 (OpenAI Embeddings 을 기반으로 추정되며 모델에 따라 다릅니다).', + vectorSpaceTooltip: + '벡터 공간은 LLM 이 데이터를 이해하는 데 필요한 장기 기억 시스템입니다.', documentProcessingPriority: '문서 처리 우선순위', - documentProcessingPriorityTip: '더 높은 문서 처리 우선순위를 원하시면 요금제를 업그레이드하세요.', - documentProcessingPriorityUpgrade: '더 높은 정확성과 빠른 속도로 데이터를 처리합니다.', + documentProcessingPriorityTip: + '더 높은 문서 처리 우선순위를 원하시면 요금제를 업그레이드하세요.', + documentProcessingPriorityUpgrade: + '더 높은 정확성과 빠른 속도로 데이터를 처리합니다.', priority: { 'standard': '표준', 'priority': '우선', @@ -60,31 +64,35 @@ const translation = { workflow: '워크플로우', llmLoadingBalancing: 'LLM 로드 밸런싱', bulkUpload: '문서 대량 업로드', - llmLoadingBalancingTooltip: '모델에 여러 API 키를 추가하여 API 속도 제한을 효과적으로 우회할 수 있습니다.', + llmLoadingBalancingTooltip: + '모델에 여러 API 키를 추가하여 API 속도 제한을 효과적으로 우회할 수 있습니다.', }, comingSoon: '곧 출시 예정', member: '멤버', memberAfter: '멤버', messageRequest: { title: '메시지 크레딧', - tooltip: 'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.', + tooltip: + 'GPT 제외 다양한 요금제에서의 메시지 호출 쿼터 (gpt4 제외). 제한을 초과하는 메시지는 OpenAI API 키를 사용합니다.', titlePerMonth: '{{count,number}} 메시지/월', }, annotatedResponse: { title: '주석 응답 쿼터', - tooltip: '수동으로 편집 및 응답 주석 달기로 앱의 사용자 정의 가능한 고품질 질의응답 기능을 제공합니다 (채팅 앱에만 해당).', + tooltip: + '수동으로 편집 및 응답 주석 달기로 앱의 사용자 정의 가능한 고품질 질의응답 기능을 제공합니다 (채팅 앱에만 해당).', }, - ragAPIRequestTooltip: 'Dify 의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.', + ragAPIRequestTooltip: + 'Dify 의 지식베이스 처리 기능을 호출하는 API 호출 수를 나타냅니다.', receiptInfo: '팀 소유자 및 팀 관리자만 구독 및 청구 정보를 볼 수 있습니다', annotationQuota: 'Annotation Quota(주석 할당량)', documentsUploadQuota: '문서 업로드 할당량', - freeTrialTipPrefix: '가입하고 받으세요', + freeTrialTipPrefix: '요금제에 가입하고 ', comparePlanAndFeatures: '계획 및 기능 비교', documents: '{{count,number}} 지식 문서', apiRateLimit: 'API 요금 한도', cloud: '클라우드 서비스', unlimitedApiRate: 'API 호출 속도 제한 없음', - freeTrialTip: '200 회의 OpenAI 호출에 대한 무료 체험.', + freeTrialTip: '200 회의 OpenAI 호출 무료 체험을 받으세요. ', annualBilling: '연간 청구', getStarted: '시작하기', apiRateLimitUnit: '{{count,number}}/일', @@ -94,10 +102,13 @@ const translation = { teamMember_other: '{{count,number}} 팀원', teamMember_one: '{{count,number}} 팀원', priceTip: '작업 공간당/', - apiRateLimitTooltip: 'Dify API 를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.', + apiRateLimitTooltip: + 'Dify API 를 통한 모든 요청에는 API 요금 한도가 적용되며, 여기에는 텍스트 생성, 채팅 대화, 워크플로 실행 및 문서 처리가 포함됩니다.', documentsRequestQuota: '{{count,number}}/분 지식 요청 비율 제한', - documentsTooltip: '지식 데이터 소스에서 가져올 수 있는 문서 수에 대한 쿼터.', - documentsRequestQuotaTooltip: '지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1 분 이내에 10 회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1 분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.', + documentsTooltip: + '지식 데이터 소스에서 가져올 수 있는 문서 수에 대한 쿼터.', + documentsRequestQuotaTooltip: + '지식 기반 내에서 작업 공간이 분당 수행할 수 있는 총 작업 수를 지정합니다. 여기에는 데이터 세트 생성, 삭제, 업데이트, 문서 업로드, 수정, 보관 및 지식 기반 쿼리가 포함됩니다. 이 지표는 지식 기반 요청의 성능을 평가하는 데 사용됩니다. 예를 들어, 샌드박스 사용자가 1 분 이내에 10 회의 연속 히트 테스트를 수행하면, 해당 작업 공간은 다음 1 분 동안 데이터 세트 생성, 삭제, 업데이트 및 문서 업로드 또는 수정과 같은 작업을 수행하는 것이 일시적으로 제한됩니다.', }, plans: { sandbox: { @@ -108,9 +119,10 @@ const translation = { }, professional: { name: '프로페셔널', - description: '개인 및 소규모 팀을 위해 더 많은 파워를 저렴한 가격에 제공합니다.', + description: + '개인 및 소규모 팀을 위해 더 많은 파워를 저렴한 가격에 제공합니다.', includesTitle: '무료 플랜에 추가로 포함된 항목:', - for: '독립 개발자/소규모 팀을 위한', + for: '1인 개발자/소규모 팀을 위한', }, team: { name: '팀', @@ -120,7 +132,8 @@ const translation = { }, enterprise: { name: '엔터프라이즈', - description: '대규모 미션 크리티컬 시스템을 위한 완전한 기능과 지원을 제공합니다.', + description: + '대규모 미션 크리티컬 시스템을 위한 완전한 기능과 지원을 제공합니다.', includesTitle: '팀 플랜에 추가로 포함된 항목:', features: { 2: '독점 기업 기능', @@ -178,7 +191,8 @@ const translation = { contactUs: '문의하기', fullTip1: '업그레이드하여 더 많은 앱을 만들기', fullTip2: '계획 한도에 도달했습니다.', - fullTip2des: '비활성 애플리케이션을 정리하여 사용량을 줄이거나 저희에게 문의하는 것이 좋습니다.', + fullTip2des: + '비활성 애플리케이션을 정리하여 사용량을 줄이거나 저희에게 문의하는 것이 좋습니다.', fullTip1des: '이 계획에서 앱을 구축할 수 있는 한계에 도달했습니다.', }, annotatedResponse: { @@ -192,7 +206,8 @@ const translation = { teamMembers: '팀원들', buildApps: '앱 만들기', documentsUploadQuota: '문서 업로드 한도', - vectorSpaceTooltip: '고품질 색인 모드를 사용하는 문서는 지식 데이터 저장소 자원을 소모합니다. 지식 데이터 저장소가 한도에 도달하면 새 문서를 업로드할 수 없습니다.', + vectorSpaceTooltip: + '고품질 색인 모드를 사용하는 문서는 지식 데이터 저장소 자원을 소모합니다. 지식 데이터 저장소가 한도에 도달하면 새 문서를 업로드할 수 없습니다.', }, teamMembers: '팀원들', } diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts index c0d64e71e0..360eb5183b 100644 --- a/web/i18n/ko-KR/common.ts +++ b/web/i18n/ko-KR/common.ts @@ -58,6 +58,8 @@ const translation = { format: '형식', more: '더 많은', downloadSuccess: '다운로드 완료.', + selectAll: '모두 선택', + deSelectAll: '모두 선택 해제', }, placeholder: { input: '입력해주세요', @@ -463,7 +465,6 @@ const translation = { apiBasedExtension: { title: 'API 기반 확장은 Dify 애플리케이션 전체에서 간편한 사용을 위한 설정을 단순화하고 집중적인 API 관리를 제공합니다.', link: '사용자 정의 API 기반 확장을 개발하는 방법 배우기', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'API 기반 확장 추가', selector: { title: 'API 기반 확장', diff --git a/web/i18n/ko-KR/dataset-creation.ts b/web/i18n/ko-KR/dataset-creation.ts index 33f6e332a0..9449d40550 100644 --- a/web/i18n/ko-KR/dataset-creation.ts +++ b/web/i18n/ko-KR/dataset-creation.ts @@ -52,7 +52,6 @@ const translation = { failed: '생성에 실패했습니다', }, website: { - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', limit: '한계', options: '옵션', firecrawlDoc: 'Firecrawl 문서', @@ -86,7 +85,6 @@ const translation = { waterCrawlNotConfiguredDescription: 'API 키로 Watercrawl 을 구성하여 사용하십시오.', watercrawlTitle: 'Watercrawl 로 웹 콘텐츠 추출하기', configureFirecrawl: '파이어크롤 구성하기', - watercrawlDocLink: '웹사이트에서 동기화하기', configureJinaReader: '지나 리더 설정하기', waterCrawlNotConfigured: 'Watercrawl 이 설정되어 있지 않습니다.', configureWatercrawl: '워터크롤 구성하기', diff --git a/web/i18n/ko-KR/dataset-documents.ts b/web/i18n/ko-KR/dataset-documents.ts index a379318959..77c94c7466 100644 --- a/web/i18n/ko-KR/dataset-documents.ts +++ b/web/i18n/ko-KR/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: '삭제', enableWarning: '아카이브된 파일은 활성화할 수 없습니다.', sync: '동기화', + resume: '이력서', + pause: '일시 중지', }, index: { enable: '활성화', @@ -388,6 +390,8 @@ const translation = { addChunk: '청크 추가 (Add Chunk)', characters_other: '문자', regeneratingMessage: '시간이 걸릴 수 있으니 잠시만 기다려 주십시오...', + keywordDuplicate: '키워드가 이미 존재합니다.', + keywordEmpty: '키워드는 비워둘 수 없습니다.', }, } diff --git a/web/i18n/ko-KR/dataset.ts b/web/i18n/ko-KR/dataset.ts index 3eb4634194..ddf4d82450 100644 --- a/web/i18n/ko-KR/dataset.ts +++ b/web/i18n/ko-KR/dataset.ts @@ -178,6 +178,7 @@ const translation = { checkName: { empty: '메타데이터 이름은 비어 있을 수 없습니다.', invalid: '메타데이터 이름은 소문자, 숫자 및 밑줄만 포함할 수 있으며 소문자로 시작해야 합니다.', + tooLong: '메타데이터 이름은 {{max}}자를 초과할 수 없습니다.', }, batchEditMetadata: { multipleValue: '다중 값', diff --git a/web/i18n/ko-KR/education.ts b/web/i18n/ko-KR/education.ts index eba00b0f9f..2dfc111d46 100644 --- a/web/i18n/ko-KR/education.ts +++ b/web/i18n/ko-KR/education.ts @@ -2,7 +2,8 @@ const translation = { toVerifiedTip: { end: 'Dify 프로페셔널 플랜을 위해.', coupon: '독점 100% 쿠폰', - front: '당신은 이제 교육 인증 상태를 받을 자격이 있습니다. 아래에 귀하의 교육 정보를 입력하여 과정을 완료하고 인증을 받으십시오.', + front: + '당신은 이제 교육 인증 상태를 받을 자격이 있습니다. 아래에 귀하의 교육 정보를 입력하여 과정을 완료하고 인증을 받으십시오.', }, form: { schoolName: { @@ -26,15 +27,18 @@ const translation = { privacyPolicy: '개인정보 보호정책', }, option: { - inSchool: '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify 는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.', - age: '나는 최소한 18 세 이상임을 확인합니다.', + inSchool: + '나는 제공된 기관에 재학 중이거나 고용되어 있음을 확인합니다. Dify 는 재학증명서나 고용증명서를 요청할 수 있습니다. 만약 내가 자격을 허위로 진술하면, 나는 내 교육 상태에 따라 처음 면제된 수수료를 지불하기로 동의합니다.', + age: '만 18세 이상입니다.', }, title: '약관 및 동의사항', }, }, submit: '제출', - rejectContent: '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.', - successContent: '귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1 년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.', + rejectContent: + '안타깝게도, 귀하는 교육 인증 상태에 적합하지 않으므로 이 이메일 주소를 사용할 경우 Dify Professional Plan 의 독점 100% 쿠폰을 받을 수 없습니다.', + successContent: + '귀하의 계정에 Dify Professional 플랜을 위한 100% 할인 쿠폰을 발급했습니다. 이 쿠폰은 1 년간 유효하므로 유효 기간 내에 사용해 주시기 바랍니다.', currentSigned: '현재 로그인 중입니다', toVerified: '교육 인증 받기', rejectTitle: '귀하의 Dify 교육 인증이 거부되었습니다.', diff --git a/web/i18n/ko-KR/plugin-tags.ts b/web/i18n/ko-KR/plugin-tags.ts index ddd75ef3a6..bd6e2fed77 100644 --- a/web/i18n/ko-KR/plugin-tags.ts +++ b/web/i18n/ko-KR/plugin-tags.ts @@ -16,7 +16,7 @@ const translation = { image: '이미지', design: '디자인', business: '사업', - agent: '대리인', + agent: '에이전트', }, allTags: '모든 태그', searchTags: '검색 태그', diff --git a/web/i18n/ko-KR/plugin.ts b/web/i18n/ko-KR/plugin.ts index 9fae9a71ac..db94226a10 100644 --- a/web/i18n/ko-KR/plugin.ts +++ b/web/i18n/ko-KR/plugin.ts @@ -137,6 +137,7 @@ const translation = { uploadingPackage: '{{packageName}} 업로드 중...', dropPluginToInstall: '플러그인 패키지를 여기에 놓아 설치하십시오.', cancel: '취소', + installWarning: '이 플러그인은 설치할 수 없습니다.', }, installFromGitHub: { uploadFailed: '업로드 실패', @@ -198,7 +199,6 @@ const translation = { endpointsEnabled: '{{num}}개의 엔드포인트 집합이 활성화되었습니다.', installFrom: '에서 설치', allCategories: '모든 카테고리', - submitPlugin: '플러그인 제출', findMoreInMarketplace: 'Marketplace 에서 더 알아보기', searchCategories: '검색 카테고리', search: '검색', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: '현재 Dify 버전이 이 플러그인과 호환되지 않습니다. 필요한 최소 버전으로 업그레이드하십시오: {{minimalDifyVersion}}', requestAPlugin: '플러그인을 요청하세요', + publishPlugins: '플러그인 게시', } export default translation diff --git a/web/i18n/ko-KR/time.ts b/web/i18n/ko-KR/time.ts index 172bb78bd6..1233dbf979 100644 --- a/web/i18n/ko-KR/time.ts +++ b/web/i18n/ko-KR/time.ts @@ -1,12 +1,12 @@ const translation = { daysInWeek: { + Sun: '일요일', + Mon: '월요일', + Tue: '화요일', Wed: '수요일', Thu: '목요일', - Fri: '자유', + Fri: '금요일', Sat: '토요일', - Sun: '태양', - Tue: '화요일', - Mon: '몬', }, months: { May: '5 월', @@ -26,7 +26,7 @@ const translation = { pickDate: '날짜 선택', cancel: '취소', ok: '좋아요', - now: '지금', + now: '오늘', }, title: { pickTime: '시간 선택', diff --git a/web/i18n/ko-KR/tools.ts b/web/i18n/ko-KR/tools.ts index 45c63b5f80..f660790265 100644 --- a/web/i18n/ko-KR/tools.ts +++ b/web/i18n/ko-KR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '추가', added: '추가됨', manageInTools: '도구에서 관리', - emptyTitle: '사용 가능한 워크플로우 도구 없음', - emptyTip: '"워크플로우 -> 도구로 등록하기"로 이동', - emptyTipCustom: '사용자 지정 도구 만들기', - emptyTitleCustom: '사용 가능한 사용자 지정 도구가 없습니다.', + custom: { + title: '사용자 정의 도구 없음', + tip: '사용자 정의 도구 생성', + }, + workflow: { + title: '워크플로우 도구 없음', + tip: '스튜디오에서 워크플로우를 도구로 게시', + }, + mcp: { + title: 'MCP 도구 없음', + tip: 'MCP 서버 추가', + }, + agent: { + title: '에이전트 전략 없음', + }, }, createTool: { title: '커스텀 도구 만들기', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Agent 추리와 프롬프트를 위한 도구 호출 이름', noTools: '도구를 찾을 수 없습니다.', copyToolName: '이름 복사', + mcp: { + create: { + cardTitle: 'MCP 서버 추가 (HTTP)', + cardLink: 'MCP 서버 통합에 대해 자세히 알아보기', + }, + noConfigured: '구성되지 않은 서버', + updateTime: '업데이트됨', + toolsCount: '{count} 도구', + noTools: '사용 가능한 도구 없음', + modal: { + title: 'MCP 서버 추가 (HTTP)', + editTitle: 'MCP 서버 수정 (HTTP)', + name: '이름 및 아이콘', + namePlaceholder: 'MCP 서버 이름 지정', + serverUrl: '서버 URL', + serverUrlPlaceholder: '서버 엔드포인트 URL', + serverUrlWarning: '서버 주소를 업데이트하면 이 서버에 의존하는 응용 프로그램에 지장이 발생할 수 있습니다', + serverIdentifier: '서버 식별자', + serverIdentifierTip: '작업 공간 내에서 MCP 서버의 고유 식별자. 소문자, 숫자, 밑줄 및 하이픈만 사용 가능. 최대 24자.', + serverIdentifierPlaceholder: '고유 식별자, 예: my-mcp-server', + serverIdentifierWarning: 'ID 변경 후 기존 앱에서 서버를 인식하지 못합니다', + cancel: '취소', + save: '저장', + confirm: '추가 및 승인', + }, + delete: 'MCP 서버 제거', + deleteConfirmTitle: '{mcp}를 제거하시겠습니까?', + operation: { + edit: '편집', + remove: '제거', + }, + authorize: '권한 부여', + authorizing: '권한 부여 중...', + authorizingRequired: '권한이 필요합니다', + authorizeTip: '권한 부여 후 도구가 여기에 표시됩니다.', + update: '업데이트', + updating: '업데이트 중', + gettingTools: '도구 가져오는 중...', + updateTools: '도구 업데이트 중...', + toolsEmpty: '도구가 로드되지 않음', + getTools: '도구 가져오기', + toolUpdateConfirmTitle: '도구 목록 업데이트', + toolUpdateConfirmContent: '도구 목록을 업데이트하면 기존 앱에 영향을 줄 수 있습니다. 계속하시겠습니까?', + toolsNum: '{count} 도구가 포함됨', + onlyTool: '1개 도구 포함', + identifier: '서버 식별자 (클릭하여 복사)', + server: { + title: 'MCP 서버', + url: '서버 URL', + reGen: '서버 URL을 다시 생성하시겠습니까?', + addDescription: '설명 추가', + edit: '설명 수정', + modal: { + addTitle: 'MCP 서버를 활성화하기 위한 설명 추가', + editTitle: '설명 수정', + description: '설명', + descriptionPlaceholder: '이 도구가 수행하는 작업과 LLM이 사용하는 방법을 설명하세요.', + parameters: '매개변수', + parametersTip: '각 매개변수의 설명을 추가하여 LLM이 목적과 제한 사항을 이해할 수 있도록 도와주세요.', + parametersPlaceholder: '매개변수의 목적 및 제한 사항', + confirm: 'MCP 서버 활성화', + }, + publishTip: '앱이 게시되지 않았습니다. 먼저 앱을 게시하십시오.', + }, + }, } export default translation diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index a1c08eef4d..6a9f97862e 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -4,9 +4,9 @@ const translation = { redo: '다시 실행', editing: '편집 중', autoSaved: '자동 저장됨', - unpublished: '미발행', - published: '발행됨', - publish: '발행', + unpublished: '게시되지 않음', + published: '게시됨', + publish: '게시하기', update: '업데이트', run: '실행', running: '실행 중', @@ -38,14 +38,13 @@ const translation = { setVarValuePlaceholder: '변수 값 설정', needConnectTip: '이 단계는 아무것도 연결되어 있지 않습니다', maxTreeDepth: '분기당 최대 {{depth}} 노드 제한', - needEndNode: '종료 블록을 추가해야 합니다', - needAnswerNode: '답변 블록을 추가해야 합니다', workflowProcess: '워크플로우 과정', notRunning: '아직 실행되지 않음', previewPlaceholder: '디버깅을 시작하려면 아래 상자에 내용을 입력하세요', effectVarConfirm: { title: '변수 제거', - content: '변수가 다른 노드에서 사용되고 있습니다. 그래도 제거하시겠습니까?', + content: + '변수가 다른 노드에서 사용되고 있습니다. 그래도 제거하시겠습니까?', }, insertVarTip: '빠르게 삽입하려면 \'/\' 키를 누르세요', processData: '데이터 처리', @@ -58,10 +57,9 @@ const translation = { learnMore: '더 알아보기', copy: '복사', duplicate: '복제', - addBlock: '블록 추가', pasteHere: '여기에 붙여넣기', pointerMode: '포인터 모드', - handMode: '핸드 모드', + handMode: '드래그 모드', model: '모델', workflowAsTool: '도구로서의 워크플로우', configureRequired: '구성 필요', @@ -76,7 +74,8 @@ const translation = { overwriteAndImport: '덮어쓰기 및 가져오기', importSuccess: '가져오기 성공', syncingData: '단 몇 초 만에 데이터를 동기화할 수 있습니다.', - importDSLTip: '현재 초안을 덮어씁니다. 가져오기 전에 워크플로를 백업으로 내보냅니다.', + importDSLTip: + '현재 초안을 덮어씁니다. 가져오기 전에 워크플로우를 백업으로 내보냅니다.', parallelTip: { click: { title: '클릭', @@ -98,9 +97,11 @@ const translation = { featuresDocLink: '더 알아보세요', fileUploadTip: '이미지 업로드 기능이 파일 업로드로 업그레이드되었습니다.', featuresDescription: '웹앱 사용자 경험 향상', - ImageUploadLegacyTip: '이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.', + ImageUploadLegacyTip: + '이제 시작 양식에서 파일 형식 변수를 만들 수 있습니다. 앞으로 이미지 업로드 기능은 더 이상 지원되지 않습니다.', importWarning: '주의', - importWarningDetails: 'DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', + importWarningDetails: + 'DSL 버전 차이는 특정 기능에 영향을 미칠 수 있습니다.', openInExplore: 'Explore 에서 열기', onFailure: '실패 시', addFailureBranch: '실패 분기 추가', @@ -115,10 +116,14 @@ const translation = { versionHistory: '버전 기록', exportPNG: 'PNG 로 내보내기', referenceVar: '참조 변수', + addBlock: '노드 추가', + needAnswerNode: '답변 노드를 추가해야 합니다.', + needEndNode: '종단 노드를 추가해야 합니다.', }, env: { envPanelTitle: '환경 변수', - envDescription: '환경 변수는 개인 정보와 자격 증명을 저장하는 데 사용될 수 있습니다. 이들은 읽기 전용이며 내보내기 중에 DSL 파일과 분리할 수 있습니다.', + envDescription: + '환경 변수는 개인 정보와 자격 증명을 저장하는 데 사용될 수 있습니다. 이들은 읽기 전용이며 내보내기 중에 DSL 파일과 분리할 수 있습니다.', envPanelButton: '변수 추가', modal: { title: '환경 변수 추가', @@ -128,7 +133,10 @@ const translation = { namePlaceholder: '환경 이름', value: '값', valuePlaceholder: '환경 값', - secretTip: '민감한 정보나 데이터를 정의하는 데 사용되며, DSL 설정은 유출 방지를 위해 구성됩니다.', + secretTip: + '민감한 정보나 데이터를 정의하는 데 사용되며, DSL 설정은 유출 방지를 위해 구성됩니다.', + description: '설명', + descriptionPlaceholder: '변수에 대해 설명하세요', }, export: { title: '비밀 환경 변수를 내보내시겠습니까?', @@ -139,7 +147,8 @@ const translation = { }, chatVariable: { panelTitle: '대화 변수', - panelDescription: '대화 변수는 LLM 이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.', + panelDescription: + '대화 변수는 LLM 이 기억해야 할 대화 기록, 업로드된 파일, 사용자 선호도 등의 상호작용 정보를 저장하는 데 사용됩니다. 이들은 읽기 및 쓰기가 가능합니다.', docLink: '자세한 내용은 문서를 참조하세요.', button: '변수 추가', modal: { @@ -169,26 +178,27 @@ const translation = { placeholder: '아직 아무 것도 변경하지 않았습니다', clearHistory: '기록 지우기', hint: '힌트', - hintText: '편집 작업이 변경 기록에 추적되며, 이 세션 동안 기기에 저장됩니다. 편집기를 떠나면 이 기록이 지워집니다.', + hintText: + '편집 작업이 변경 기록에 추적되며, 이 세션 동안 기기에 저장됩니다. 편집기를 떠나면 이 기록이 지워집니다.', stepBackward_one: '{{count}} 단계 뒤로', stepBackward_other: '{{count}} 단계 뒤로', stepForward_one: '{{count}} 단계 앞으로', stepForward_other: '{{count}} 단계 앞으로', sessionStart: '세션 시작', currentState: '현재 상태', - nodeTitleChange: '블록 제목 변경됨', - nodeDescriptionChange: '블록 설명 변경됨', - nodeDragStop: '블록 이동됨', - nodeChange: '블록 변경됨', - nodeConnect: '블록 연결됨', - nodePaste: '블록 붙여넣기됨', - nodeDelete: '블록 삭제됨', - nodeAdd: '블록 추가됨', - nodeResize: '블록 크기 조정됨', noteAdd: '노트 추가됨', noteChange: '노트 변경됨', noteDelete: '노트 삭제됨', - edgeDelete: '블록 연결 해제됨', + nodeConnect: '노드가 연결되었습니다.', + nodePaste: '노드 붙여넣기', + nodeDelete: '노드가 삭제되었습니다.', + nodeAdd: '노드가 추가되었습니다.', + nodeChange: '노드가 변경되었습니다.', + nodeDescriptionChange: '노드 설명이 변경됨', + nodeResize: '노드 크기 조정됨', + nodeDragStop: '노드가 이동했습니다.', + edgeDelete: '노드가 연결이 끊어졌습니다.', + nodeTitleChange: '노드 제목이 변경됨', }, errorMsg: { fieldRequired: '{{field}}가 필요합니다', @@ -203,7 +213,8 @@ const translation = { visionVariable: '비전 변수', }, invalidVariable: '잘못된 변수', - rerankModelRequired: 'Rerank Model 을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.', + rerankModelRequired: + 'Rerank Model 을 켜기 전에 설정에서 모델이 성공적으로 구성되었는지 확인하십시오.', noValidTool: '{{field}} 유효한 도구가 선택되지 않았습니다.', toolParameterRequired: '{{field}}: 매개변수 [{{param}}] 이 필요합니다.', }, @@ -217,8 +228,6 @@ const translation = { loop: '루프', }, tabs: { - 'searchBlock': '블록 검색', - 'blocks': '블록', 'tools': '도구', 'allTool': '전체', 'builtInTool': '내장', @@ -232,6 +241,8 @@ const translation = { 'searchTool': '검색 도구', 'plugin': '플러그인', 'agent': '에이전트 전략', + 'blocks': '노드', + 'searchBlock': '검색 노드', }, blocks: { 'start': '시작', @@ -262,22 +273,34 @@ const translation = { 'end': '워크플로우의 종료 및 결과 유형을 정의합니다', 'answer': '대화의 답변 내용을 정의합니다', 'llm': '질문에 답하거나 자연어를 처리하기 위해 대형 언어 모델을 호출합니다', - 'knowledge-retrieval': '사용자 질문과 관련된 텍스트 콘텐츠를 지식 베이스에서 쿼리할 수 있습니다', - 'question-classifier': '사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다', - 'if-else': 'if/else 조건을 기반으로 워크플로우를 두 가지 분기로 나눌 수 있습니다', + 'knowledge-retrieval': + '사용자 질문과 관련된 텍스트 콘텐츠를 지식 베이스에서 쿼리할 수 있습니다', + 'question-classifier': + '사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다', + 'if-else': + 'if/else 조건을 기반으로 워크플로우를 두 가지 분기로 나눌 수 있습니다', 'code': '사용자 정의 논리를 구현하기 위해 Python 또는 NodeJS 코드를 실행합니다', - 'template-transform': 'Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다', + 'template-transform': + 'Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다', 'http-request': 'HTTP 프로토콜을 통해 서버 요청을 보낼 수 있습니다', - 'variable-assigner': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', - 'assigner': '변수 할당 노드는 쓰기 가능한 변수 (대화 변수 등) 에 값을 할당하는 데 사용됩니다.', - 'variable-aggregator': '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', - 'iteration': '목록 객체에서 여러 단계를 수행하여 모든 결과가 출력될 때까지 반복합니다.', - 'parameter-extractor': '도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.', - 'document-extractor': '업로드된 문서를 LLM 에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.', + 'variable-assigner': + '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', + 'assigner': + '변수 할당 노드는 쓰기 가능한 변수 (대화 변수 등) 에 값을 할당하는 데 사용됩니다.', + 'variable-aggregator': + '다중 분기 변수들을 하나의 변수로 집계하여 다운스트림 노드의 통합 구성을 가능하게 합니다.', + 'iteration': + '목록 객체에서 여러 단계를 수행하여 모든 결과가 출력될 때까지 반복합니다.', + 'parameter-extractor': + '도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.', + 'document-extractor': + '업로드된 문서를 LLM 에서 쉽게 이해할 수 있는 텍스트 콘텐츠로 구문 분석하는 데 사용됩니다.', 'list-operator': '배열 내용을 필터링하거나 정렬하는 데 사용됩니다.', - 'agent': '질문에 답하거나 자연어를 처리하기 위해 대규모 언어 모델을 호출하는 경우', + 'agent': + '질문에 답하거나 자연어를 처리하기 위해 대규모 언어 모델을 호출하는 경우', 'loop': '종료 조건이 충족되거나 최대 반복 횟수에 도달할 때까지 논리 루프를 실행합니다.', - 'loop-end': '"break"와 동일합니다. 이 노드는 구성 항목이 없습니다. 루프 본문이 이 노드에 도달하면 루프가 종료됩니다.', + 'loop-end': + '"break"와 동일합니다. 이 노드는 구성 항목이 없습니다. 루프 본문이 이 노드에 도달하면 루프가 종료됩니다.', }, operator: { zoomIn: '확대', @@ -288,21 +311,23 @@ const translation = { }, panel: { userInputField: '사용자 입력 필드', - changeBlock: '블록 변경', helpLink: '도움말 링크', about: '정보', createdBy: '작성자 ', nextStep: '다음 단계', - addNextStep: '이 워크플로우의 다음 블록 추가', - selectNextStep: '다음 블록 선택', runThisStep: '이 단계 실행', checklist: '체크리스트', checklistTip: '게시하기 전에 모든 문제가 해결되었는지 확인하세요', checklistResolved: '모든 문제가 해결되었습니다', - organizeBlocks: '블록 정리', change: '변경', optional: '(선택사항)', moveToThisNode: '이 노드로 이동', + organizeBlocks: '노드 정리하기', + selectNextStep: '다음 단계 선택', + changeBlock: '노드 변경', + addNextStep: '이 워크플로우에 다음 단계를 추가하세요.', + minimize: '전체 화면 종료', + maximize: '캔버스 전체 화면', }, nodes: { common: { @@ -336,9 +361,12 @@ const translation = { failBranch: { title: '실패 분기', desc: '오류가 발생하면 예외 분기를 실행합니다', - customize: '캔버스로 이동하여 fail branch logic 를 사용자 지정합니다.', - inLog: '노드 예외는 실패 분기를 자동으로 실행합니다. 노드 출력은 오류 유형 및 오류 메시지를 반환하고 다운스트림으로 전달합니다.', - customizeTip: 'fail 분기가 활성화되면 노드에서 throw 된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.', + customize: + '캔버스로 이동하여 fail branch logic 를 사용자 지정합니다.', + inLog: + '노드 예외는 실패 분기를 자동으로 실행합니다. 노드 출력은 오류 유형 및 오류 메시지를 반환하고 다운스트림으로 전달합니다.', + customizeTip: + 'fail 분기가 활성화되면 노드에서 throw 된 예외가 프로세스를 종료하지 않습니다. 대신 미리 정의된 실패 분기를 자동으로 실행하여 오류 메시지, 보고서, 수정 사항을 유연하게 제공하거나 작업을 건너뛸 수 있습니다.', }, partialSucceeded: { tip: '프로세스에 {{num}} 노드가 비정상적으로 실행 중입니다. 추적으로 이동하여 로그를 확인하십시오.', @@ -356,10 +384,11 @@ const translation = { retrySuccessful: '재시도 성공', retryFailed: '재시도 실패', retryFailedTimes: '{{times}} 재시도 실패', - times: '배', + times: '번', ms: '미에스', retries: '{{숫자}} 재시도', }, + typeSwitch: {}, }, start: { required: '필수', @@ -397,7 +426,8 @@ const translation = { variables: '변수', context: '컨텍스트', contextTooltip: '컨텍스트로 지식을 가져올 수 있습니다', - notSetContextInPromptTip: '컨텍스트 기능을 활성화하려면 PROMPT 에 컨텍스트 변수를 입력하세요.', + notSetContextInPromptTip: + '컨텍스트 기능을 활성화하려면 PROMPT 에 컨텍스트 변수를 입력하세요.', prompt: '프롬프트', roleDescription: { system: '대화를 위한 고급 지침 제공', @@ -441,8 +471,10 @@ const translation = { stringValidations: '문자열 검증', showAdvancedOptions: '고급 옵션 표시', promptPlaceholder: '당신의 JSON 스키마를 설명하세요...', - generationTip: '자연어를 사용하여 JSON 스키마를 신속하게 생성할 수 있습니다.', - resultTip: '여기 생성된 결과가 있습니다. 만약 만족하지 않으신다면, 돌아가서 프롬프트를 수정할 수 있습니다.', + generationTip: + '자연어를 사용하여 JSON 스키마를 신속하게 생성할 수 있습니다.', + resultTip: + '여기 생성된 결과가 있습니다. 만약 만족하지 않으신다면, 돌아가서 프롬프트를 수정할 수 있습니다.', regenerate: '재생하다', required: '필수', doc: '구조화된 출력에 대해 더 알아보세요.', @@ -468,7 +500,8 @@ const translation = { }, automatic: { desc: '쿼리 변수를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.', - subTitle: '사용자 쿼리를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.', + subTitle: + '사용자 쿼리를 기반으로 메타데이터 필터링 조건을 자동으로 생성합니다.', title: '자동', }, manual: { @@ -535,12 +568,17 @@ const translation = { title: 'cURL 에서 가져오기', placeholder: '여기에 cURL 문자열 붙여 넣기', }, + verifySSL: { + title: 'SSL 인증서 확인', + warningTooltip: 'SSL 검증을 비활성화하는 것은 프로덕션 환경에서는 권장되지 않습니다. 이는 연결이 중간자 공격과 같은 보안 위협에 취약하게 만들므로 개발 또는 테스트에서만 사용해야 합니다.', + }, }, code: { inputVars: '입력 변수', outputVars: '출력 변수', advancedDependencies: '고급 종속성', - advancedDependenciesTip: '더 많은 시간이 소요되거나 기본으로 내장되지 않은 일부 미리 로드된 종속성을 여기에 추가하세요', + advancedDependenciesTip: + '더 많은 시간이 소요되거나 기본으로 내장되지 않은 일부 미리 로드된 종속성을 여기에 추가하세요', searchDependencies: '종속성 검색', }, templateTransform: { @@ -554,7 +592,8 @@ const translation = { ifElse: { if: 'If', else: 'Else', - elseDescription: 'If 조건이 충족되지 않을 때 실행할 논리를 정의하는 데 사용됩니다.', + elseDescription: + 'If 조건이 충족되지 않을 때 실행할 논리를 정의하는 데 사용됩니다.', and: '그리고', or: '또는', operator: '연산자', @@ -607,7 +646,8 @@ const translation = { array: '배열', }, aggregationGroup: '집계 그룹', - aggregationGroupTip: '이 기능을 활성화하면 변수 집계자가 여러 변수 집합을 집계할 수 있습니다.', + aggregationGroupTip: + '이 기능을 활성화하면 변수 집계자가 여러 변수 집합을 집계할 수 있습니다.', addGroup: '그룹 추가', outputVars: { varDescribe: '{{groupName}} 출력', @@ -643,7 +683,8 @@ const translation = { 'noAssignedVars': '사용 가능한 할당된 변수가 없습니다.', 'noVarTip': '"+" 버튼을 클릭하여 변수를 추가합니다.', 'setParameter': '매개 변수 설정...', - 'assignedVarsDescription': '할당된 변수는 대화 변수와 같은 쓰기 가능한 변수여야 합니다.', + 'assignedVarsDescription': + '할당된 변수는 대화 변수와 같은 쓰기 가능한 변수여야 합니다.', 'selectAssignedVariable': '할당된 변수 선택...', 'varNotSet': '변수가 설정되지 않음', }, @@ -667,6 +708,7 @@ const translation = { inputVars: '입력 변수', outputVars: { className: '클래스 이름', + usage: '모델 사용 정보', }, class: '클래스', classNamePlaceholder: '클래스 이름을 작성하세요', @@ -675,11 +717,17 @@ const translation = { topicPlaceholder: '주제 이름을 작성하세요', addClass: '클래스 추가', instruction: '지시', - instructionTip: '질문 분류기가 질문을 더 잘 분류할 수 있도록 추가 지시를 입력하세요.', + instructionTip: + '질문 분류기가 질문을 더 잘 분류할 수 있도록 추가 지시를 입력하세요.', instructionPlaceholder: '지시를 작성하세요', }, parameterExtractor: { inputVar: '입력 변수', + outputVars: { + isSuccess: '성공 여부. 성공 시 값은 1 이고, 실패 시 값은 0 입니다.', + errorReason: '오류 원인', + usage: '모델 사용 정보', + }, extractParameters: '매개변수 추출', importFromTool: '도구에서 가져오기', addExtractParameter: '추출 매개변수 추가', @@ -691,14 +739,17 @@ const translation = { description: '설명', descriptionPlaceholder: '추출 매개변수 설명', required: '필수', - requiredContent: '필수는 모델 추론을 위한 참고 용도로만 사용되며, 매개변수 출력의 필수 유효성 검사는 아닙니다.', + requiredContent: + '필수는 모델 추론을 위한 참고 용도로만 사용되며, 매개변수 출력의 필수 유효성 검사는 아닙니다.', }, extractParametersNotSet: '추출 매개변수가 설정되지 않음', instruction: '지시', - instructionTip: '매개변수 추출기가 매개변수를 추출하는 방법을 이해하는 데 도움이 되는 추가 지시를 입력하세요.', + instructionTip: + '매개변수 추출기가 매개변수를 추출하는 방법을 이해하는 데 도움이 되는 추가 지시를 입력하세요.', advancedSetting: '고급 설정', reasoningMode: '추론 모드', - reasoningModeTip: '모델의 함수 호출 또는 프롬프트에 대한 지시 응답 능력을 기반으로 적절한 추론 모드를 선택할 수 있습니다.', + reasoningModeTip: + '모델의 함수 호출 또는 프롬프트에 대한 지시 응답 능력을 기반으로 적절한 추론 모드를 선택할 수 있습니다.', isSuccess: '성공 여부. 성공 시 값은 1 이고, 실패 시 값은 0 입니다.', errorReason: '오류 원인', }, @@ -724,9 +775,12 @@ const translation = { error_other: '{{개수}} 오류', parallelModeEnableTitle: 'Parallel Mode Enabled(병렬 모드 사용)', parallelPanelDesc: '병렬 모드에서 반복의 작업은 병렬 실행을 지원합니다.', - parallelModeEnableDesc: '병렬 모드에서는 반복 내의 작업이 병렬 실행을 지원합니다. 오른쪽의 속성 패널에서 이를 구성할 수 있습니다.', - MaxParallelismDesc: '최대 병렬 처리는 단일 반복에서 동시에 실행되는 작업 수를 제어하는 데 사용됩니다.', - answerNodeWarningDesc: '병렬 모드 경고: 응답 노드, 대화 변수 할당 및 반복 내의 지속적인 읽기/쓰기 작업으로 인해 예외가 발생할 수 있습니다.', + parallelModeEnableDesc: + '병렬 모드에서는 반복 내의 작업이 병렬 실행을 지원합니다. 오른쪽의 속성 패널에서 이를 구성할 수 있습니다.', + MaxParallelismDesc: + '최대 병렬 처리는 단일 반복에서 동시에 실행되는 작업 수를 제어하는 데 사용됩니다.', + answerNodeWarningDesc: + '병렬 모드 경고: 응답 노드, 대화 변수 할당 및 반복 내의 지속적인 읽기/쓰기 작업으로 인해 예외가 발생할 수 있습니다.', }, note: { editor: { @@ -776,12 +830,14 @@ const translation = { agent: { strategy: { label: '에이전트 전략', - tooltip: '다양한 에이전트 전략은 시스템이 다단계 도구 호출을 계획하고 실행하는 방법을 결정합니다', + tooltip: + '다양한 에이전트 전략은 시스템이 다단계 도구 호출을 계획하고 실행하는 방법을 결정합니다', configureTip: '에이전트 전략을 구성하세요.', searchPlaceholder: '검색 에이전트 전략', shortLabel: '전략', selectTip: '에이전트 전략 선택', - configureTipDesc: '에이전트 전략을 구성한 후 이 노드는 나머지 구성을 자동으로 로드합니다. 이 전략은 다단계 도구 추론의 메커니즘에 영향을 미칩니다.', + configureTipDesc: + '에이전트 전략을 구성한 후 이 노드는 나머지 구성을 자동으로 로드합니다. 이 전략은 다단계 도구 추론의 메커니즘에 영향을 미칩니다.', }, pluginInstaller: { install: '설치하다', @@ -794,7 +850,8 @@ const translation = { }, modelNotSupport: { title: '지원되지 않는 모델', - descForVersionSwitch: '설치된 플러그인 버전은 이 모델을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', + descForVersionSwitch: + '설치된 플러그인 버전은 이 모델을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', desc: '설치된 플러그인 버전은 이 모델을 제공하지 않습니다.', }, modelSelectorTooltips: { @@ -821,13 +878,17 @@ const translation = { cancel: '취소', title: '플러그인 설치', }, - strategyNotFoundDescAndSwitchVersion: '설치된 플러그인 버전은 이 전략을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', + strategyNotFoundDescAndSwitchVersion: + '설치된 플러그인 버전은 이 전략을 제공하지 않습니다. 버전을 전환하려면 클릭합니다.', learnMore: '더 알아보세요', toolNotAuthorizedTooltip: '{{도구}} 권한이 부여되지 않음', - strategyNotFoundDesc: '설치된 플러그인 버전은 이 전략을 제공하지 않습니다.', + strategyNotFoundDesc: + '설치된 플러그인 버전은 이 전략을 제공하지 않습니다.', maxIterations: '최대 반복 횟수', - pluginNotFoundDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', - pluginNotInstalledDesc: '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', + pluginNotFoundDesc: + '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', + pluginNotInstalledDesc: + '이 플러그인은 GitHub 에서 설치됩니다. 플러그인으로 이동하여 다시 설치하십시오.', strategyNotInstallTooltip: '{{strategy}}가 설치되지 않았습니다.', tools: '도구', unsupportedStrategy: '지원되지 않는 전략', @@ -866,9 +927,11 @@ const translation = { loopVariables: '루프 변수', setLoopVariables: '루프 범위 내에서 변수를 설정합니다.', initialLoopVariables: '초기 루프 변수', - breakConditionTip: '종료 조건과 대화 변수가 있는 루프 내에서만 변수를 참조할 수 있습니다.', + breakConditionTip: + '종료 조건과 대화 변수가 있는 루프 내에서만 변수를 참조할 수 있습니다.', currentLoopCount: '현재 루프 카운트: {{count}}', - loopMaxCountError: '유효한 최대 루프 수를 입력하십시오. 범위는 1 에서 {{maxCount}}입니다.', + loopMaxCountError: + '유효한 최대 루프 수를 입력하십시오. 범위는 1 에서 {{maxCount}}입니다.', totalLoopCount: '총 루프 횟수: {{count}}', variableName: '변수 이름', loopNode: '루프 노드', @@ -883,7 +946,8 @@ const translation = { conversationVars: '대화 변수', noVarsForOperation: '선택한 작업에 할당할 수 있는 변수가 없습니다.', noAssignedVars: '사용 가능한 할당된 변수가 없습니다.', - assignedVarsDescription: '할당된 변수는 다음과 같이 쓰기 가능한 변수여야 합니다.', + assignedVarsDescription: + '할당된 변수는 다음과 같이 쓰기 가능한 변수여야 합니다.', }, versionHistory: { filter: { @@ -897,7 +961,8 @@ const translation = { titleLengthLimit: '제목은 {{limit}}자를 초과할 수 없습니다.', title: '제목', releaseNotes: '릴리스 노트', - releaseNotesLengthLimit: '릴리스 노트는 {{limit}}자를 초과할 수 없습니다.', + releaseNotesLengthLimit: + '릴리스 노트는 {{limit}}자를 초과할 수 없습니다.', }, action: { updateFailure: '버전 업데이트에 실패했습니다.', @@ -912,11 +977,41 @@ const translation = { currentDraft: '현재 초안', releaseNotesPlaceholder: '변경된 내용을 설명하세요.', defaultName: '제목 없는 버전', - nameThisVersion: '이 버전의 이름을 지어주세요', - title: '버전들', + nameThisVersion: '이름 바꾸기', + title: '버전 기록', deletionTip: '삭제는 되돌릴 수 없으니, 확인해 주시기 바랍니다.', restorationTip: '버전 복원 후 현재 초안이 덮어쓰여질 것입니다.', }, + debug: { + noData: { + runThisNode: '이 노드를 실행하세요', + description: '마지막 실행 결과가 여기 표시됩니다.', + }, + variableInspect: { + trigger: { + stop: '멈춰 뛰어', + clear: '맑은', + running: '캐싱 실행 상태', + cached: '캐시된 변수를 보기', + normal: '변수 검사', + }, + title: '변수 검사', + view: '로그 보기', + edited: '편집됨', + emptyLink: '더 알아보기', + chatNode: '대화', + clearAll: '모두 초기화', + systemNode: '시스템', + envNode: '환경', + clearNode: '캐시된 변수를 지우기', + resetConversationVar: '대화 변수를 기본 값으로 재설정합니다.', + reset: '마지막 실행 값으로 재설정', + emptyTip: + '캔버스에서 노드를 한 단계씩 실행한 후, 변수 검사에서 노드 변수의 현재 값을 볼 수 있습니다.', + }, + settingsTab: '설정', + lastRunTab: '마지막 실행', + }, } export default translation diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 48b44c0cbb..dcd286d351 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -360,6 +360,7 @@ const translation = { placeholder: 'Tutaj napisz swoją wiadomość wprowadzającą, możesz użyć zmiennych, spróbuj wpisać {{variable}}.', openingQuestion: 'Pytania otwierające', + openingQuestionPlaceholder: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}.', noDataPlaceHolder: 'Rozpoczynanie rozmowy z użytkownikiem może pomóc AI nawiązać bliższe połączenie z nim w aplikacjach konwersacyjnych.', varTip: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 856a64c868..141ab190e0 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -99,6 +99,7 @@ const translation = { agentUserDescription: 'Inteligentny agent zdolny do iteracyjnego wnioskowania i autonomicznego wykorzystania narzędzi do osiągania celów zadań.', workflowShortDescription: 'Agentowy przepływ dla inteligentnych automatyzacji', advancedUserDescription: 'Przepływ z dodatkowymi funkcjami pamięci i interfejsem chatbota.', + dropDSLToCreateApp: 'Upuść plik DSL tutaj, aby utworzyć aplikację', }, editApp: 'Edytuj informacje', editAppTitle: 'Edytuj informacje o aplikacji', @@ -142,6 +143,14 @@ const translation = { notConfigured: 'Skonfiguruj dostawcę, aby włączyć śledzenie', moreProvider: 'Więcej dostawców', }, + arize: { + title: 'Arize', + description: 'Obserwowalność LLM klasy korporacyjnej, ocena online i offline, monitorowanie i eksperymentowanie — oparta na OpenTelemetry. Zaprojektowana specjalnie dla aplikacji opartych na LLM i agentach.', + }, + phoenix: { + title: 'Phoenix', + description: 'Otwarta i oparta na OpenTelemetry platforma do obserwowalności, oceny, inżynierii promptów i eksperymentowania dla Twoich przepływów pracy i agentów LLM.', + }, langsmith: { title: 'LangSmith', description: 'Kompleksowa platforma deweloperska dla każdego etapu cyklu życia aplikacji opartej na LLM.', @@ -170,6 +179,7 @@ const translation = { title: 'Tkaj', description: 'Weave to platforma open-source do oceny, testowania i monitorowania aplikacji LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', diff --git a/web/i18n/pl-PL/common.ts b/web/i18n/pl-PL/common.ts index e081a1ed9e..b8bf43a6b0 100644 --- a/web/i18n/pl-PL/common.ts +++ b/web/i18n/pl-PL/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'Pobieranie nie powiodło się. Proszę spróbować ponownie później.', more: 'Więcej', downloadSuccess: 'Pobieranie zakończone.', + deSelectAll: 'Odznacz wszystkie', + selectAll: 'Zaznacz wszystkie', }, placeholder: { input: 'Proszę wprowadzić', @@ -481,7 +483,6 @@ const translation = { title: 'Rozszerzenia oparte na interfejsie API zapewniają scentralizowane zarządzanie interfejsami API, upraszczając konfigurację dla łatwego użytkowania w aplikacjach Dify.', link: 'Dowiedz się, jak opracować własne rozszerzenie interfejsu API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Dodaj rozszerzenie interfejsu API', selector: { title: 'Rozszerzenie interfejsu API', diff --git a/web/i18n/pl-PL/dataset-creation.ts b/web/i18n/pl-PL/dataset-creation.ts index 236202d867..83e6eab96b 100644 --- a/web/i18n/pl-PL/dataset-creation.ts +++ b/web/i18n/pl-PL/dataset-creation.ts @@ -54,7 +54,6 @@ const translation = { }, website: { limit: 'Ograniczać', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', firecrawlDoc: 'Dokumentacja Firecrawl', unknownError: 'Nieznany błąd', fireCrawlNotConfiguredDescription: 'Skonfiguruj Firecrawl z kluczem API, aby z niego korzystać.', @@ -85,7 +84,6 @@ const translation = { jinaReaderNotConfiguredDescription: 'Skonfiguruj Jina Reader, wprowadzając bezpłatny klucz API, aby uzyskać dostęp.', watercrawlTitle: 'Wyodrębnij treści z sieci za pomocą Watercrawl', configureWatercrawl: 'Skonfiguruj Watercrawl', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureJinaReader: 'Skonfiguruj Czytnik Jina', configureFirecrawl: 'Skonfiguruj Firecrawl', watercrawlDoc: 'Dokumentacja Watercrawl', diff --git a/web/i18n/pl-PL/dataset-documents.ts b/web/i18n/pl-PL/dataset-documents.ts index 37f373ac93..e581b2d090 100644 --- a/web/i18n/pl-PL/dataset-documents.ts +++ b/web/i18n/pl-PL/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Usuń', enableWarning: 'Zarchiwizowany plik nie może zostać włączony', sync: 'Synchronizuj', + resume: 'Wznawiać', + pause: 'Pauza', }, index: { enable: 'Włącz', @@ -390,6 +392,8 @@ const translation = { newChildChunk: 'Nowy fragment podrzędny', clearFilter: 'Wyczyść filtr', childChunks_one: 'FRAGMENT POTOMNY', + keywordDuplicate: 'Słowo kluczowe już istnieje', + keywordEmpty: 'Słowo kluczowe nie może być puste', }, } diff --git a/web/i18n/pl-PL/dataset.ts b/web/i18n/pl-PL/dataset.ts index 3006c46c97..0a37698750 100644 --- a/web/i18n/pl-PL/dataset.ts +++ b/web/i18n/pl-PL/dataset.ts @@ -185,6 +185,7 @@ const translation = { checkName: { empty: 'Nazwa metadanych nie może być pusta', invalid: 'Nazwa metadanych może zawierać tylko małe litery, cyfry i podkreślenia oraz musi zaczynać się od małej litery', + tooLong: 'Nazwa metadanych nie może przekraczać {{max}} znaków', }, batchEditMetadata: { multipleValue: 'Wielokrotna wartość', diff --git a/web/i18n/pl-PL/plugin.ts b/web/i18n/pl-PL/plugin.ts index f02575919e..d5c05d0df8 100644 --- a/web/i18n/pl-PL/plugin.ts +++ b/web/i18n/pl-PL/plugin.ts @@ -51,7 +51,7 @@ const translation = { paramsTip1: 'Steruje parametrami wnioskowania LLM.', unsupportedContent: 'Zainstalowana wersja wtyczki nie zapewnia tej akcji.', params: 'KONFIGURACJA ROZUMOWANIA', - auto: 'Automatyczne', + auto: 'Auto', empty: 'Kliknij przycisk "+", aby dodać narzędzia. Możesz dodać wiele narzędzi.', descriptionLabel: 'Opis narzędzia', title: 'Dodaj narzędzie', @@ -60,7 +60,7 @@ const translation = { uninstalledContent: 'Ta wtyczka jest instalowana z repozytorium lokalnego/GitHub. Proszę użyć po instalacji.', unsupportedTitle: 'Nieobsługiwana akcja', uninstalledTitle: 'Narzędzie nie jest zainstalowane', - paramsTip2: 'Gdy opcja "Automatycznie" jest wyłączona, używana jest wartość domyślna.', + paramsTip2: 'Gdy opcja "Auto" jest wyłączona, używana jest wartość domyślna.', toolLabel: 'Narzędzie', toolSetting: 'Ustawienia narzędzi', }, @@ -137,6 +137,7 @@ const translation = { readyToInstallPackage: 'Informacje o instalacji następującej wtyczki', uploadingPackage: 'Przesyłanie {{packageName}}...', installedSuccessfully: 'Instalacja powiodła się', + installWarning: 'Ten plugin nie może być zainstalowany.', }, installFromGitHub: { installPlugin: 'Zainstaluj wtyczkę z GitHub', @@ -206,12 +207,12 @@ const translation = { fromMarketplace: 'Z Marketplace', searchPlugins: 'Wtyczki wyszukiwania', searchTools: 'Narzędzia wyszukiwania...', - submitPlugin: 'Prześlij wtyczkę', metadata: { title: 'Wtyczki', }, difyVersionNotCompatible: 'Obecna wersja Dify nie jest kompatybilna z tym wtyczką, proszę zaktualizować do minimalnej wymaganej wersji: {{minimalDifyVersion}}', requestAPlugin: 'Poproś o wtyczkę', + publishPlugins: 'Publikowanie wtyczek', } export default translation diff --git a/web/i18n/pl-PL/tools.ts b/web/i18n/pl-PL/tools.ts index e9d92d150e..183abc3f31 100644 --- a/web/i18n/pl-PL/tools.ts +++ b/web/i18n/pl-PL/tools.ts @@ -146,16 +146,92 @@ const translation = { type: 'typ', category: 'kategoria', add: 'dodawać', - emptyTitle: 'Brak dostępnego narzędzia do przepływu pracy', - emptyTip: 'Przejdź do "Przepływ pracy -> Opublikuj jako narzędzie"', - emptyTitleCustom: 'Brak dostępnego narzędzia niestandardowego', - emptyTipCustom: 'Tworzenie narzędzia niestandardowego', + custom: { + title: 'Brak dostępnego narzędzia niestandardowego', + tip: 'Utwórz narzędzie niestandardowe', + }, + workflow: { + title: 'Brak dostępnego narzędzia workflow', + tip: 'Publikuj przepływy pracy jako narzędzia w Studio', + }, + mcp: { + title: 'Brak dostępnego narzędzia MCP', + tip: 'Dodaj serwer MCP', + }, + agent: { + title: 'Brak dostępnej strategii agenta', + }, }, openInStudio: 'Otwieranie w Studio', customToolTip: 'Dowiedz się więcej o niestandardowych narzędziach Dify', toolNameUsageTip: 'Nazwa wywołania narzędzia do wnioskowania i podpowiadania agentowi', noTools: 'Nie znaleziono narzędzi', copyToolName: 'Kopiuj nazwę', + mcp: { + create: { + cardTitle: 'Dodaj serwer MCP (HTTP)', + cardLink: 'Dowiedz się więcej o integracji serwera MCP', + }, + noConfigured: 'Serwer nieskonfigurowany', + updateTime: 'Zaktualizowano', + toolsCount: '{count} narzędzi', + noTools: 'Brak dostępnych narzędzi', + modal: { + title: 'Dodaj serwer MCP (HTTP)', + editTitle: 'Edytuj serwer MCP (HTTP)', + name: 'Nazwa i ikona', + namePlaceholder: 'Nazwij swój serwer MCP', + serverUrl: 'URL serwera', + serverUrlPlaceholder: 'URL do punktu końcowego serwera', + serverUrlWarning: 'Aktualizacja adresu serwera może zakłócić działanie aplikacji od niego zależnych', + serverIdentifier: 'Identyfikator serwera', + serverIdentifierTip: 'Unikalny identyfikator serwera MCP w obszarze roboczym. Tylko małe litery, cyfry, podkreślenia i myślniki. Maks. 24 znaki.', + serverIdentifierPlaceholder: 'Unikalny identyfikator, np. my-mcp-server', + serverIdentifierWarning: 'Po zmianie ID serwer nie będzie rozpoznawany przez istniejące aplikacje', + cancel: 'Anuluj', + save: 'Zapisz', + confirm: 'Dodaj i autoryzuj', + }, + delete: 'Usuń serwer MCP', + deleteConfirmTitle: 'Usunąć {mcp}?', + operation: { + edit: 'Edytuj', + remove: 'Usuń', + }, + authorize: 'Autoryzuj', + authorizing: 'Autoryzowanie...', + authorizingRequired: 'Wymagana autoryzacja', + authorizeTip: 'Po autoryzacji narzędzia będą wyświetlane tutaj.', + update: 'Aktualizuj', + updating: 'Aktualizowanie...', + gettingTools: 'Pobieranie narzędzi...', + updateTools: 'Aktualizowanie narzędzi...', + toolsEmpty: 'Narzędzia niezaładowane', + getTools: 'Pobierz narzędzia', + toolUpdateConfirmTitle: 'Aktualizuj listę narzędzi', + toolUpdateConfirmContent: 'Aktualizacja listy narzędzi może wpłynąć na istniejące aplikacje. Kontynuować?', + toolsNum: '{count} narzędzi zawartych', + onlyTool: '1 narzędzie zawarte', + identifier: 'Identyfikator serwera (Kliknij, aby skopiować)', + server: { + title: 'Serwer MCP', + url: 'URL serwera', + reGen: 'Wygenerować ponownie URL serwera?', + addDescription: 'Dodaj opis', + edit: 'Edytuj opis', + modal: { + addTitle: 'Dodaj opis, aby aktywować serwer MCP', + editTitle: 'Edytuj opis', + description: 'Opis', + descriptionPlaceholder: 'Wyjaśnij funkcjonalność tego narzędzia i sposób użycia przez LLM', + parameters: 'Parametry', + parametersTip: 'Dodaj opisy każdego parametru, aby pomóc LLM zrozumieć ich cel i ograniczenia.', + parametersPlaceholder: 'Cel i ograniczenia parametru', + confirm: 'Aktywuj serwer MCP', + }, + publishTip: 'Aplikacja nieopublikowana. Najpierw opublikuj aplikację.', + }, + }, } export default translation diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index c91af84e3a..6d1c9ccc8c 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Ustaw zmienną', needConnectTip: 'Ten krok nie jest połączony z niczym', maxTreeDepth: 'Maksymalny limit {{depth}} węzłów na gałąź', - needEndNode: 'Należy dodać blok końcowy', - needAnswerNode: 'Należy dodać blok odpowiedzi', workflowProcess: 'Proces przepływu pracy', notRunning: 'Jeszcze nie uruchomiono', previewPlaceholder: 'Wprowadź treść w poniższym polu, aby rozpocząć debugowanie Chatbota', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Dowiedz się więcej', copy: 'Kopiuj', duplicate: 'Duplikuj', - addBlock: 'Dodaj blok', pasteHere: 'Wklej tutaj', pointerMode: 'Tryb wskaźnika', handMode: 'Tryb ręczny', @@ -115,6 +112,9 @@ const translation = { exportPNG: 'Eksportuj jako PNG', publishUpdate: 'Opublikuj aktualizację', referenceVar: 'Zmienna odniesienia', + addBlock: 'Dodaj węzeł', + needEndNode: 'Należy dodać węzeł końcowy', + needAnswerNode: 'Węzeł odpowiedzi musi zostać dodany', }, env: { envPanelTitle: 'Zmienne Środowiskowe', @@ -129,6 +129,8 @@ const translation = { value: 'Wartość', valuePlaceholder: 'wartość środowiska', secretTip: 'Używane do definiowania wrażliwych informacji lub danych, z ustawieniami DSL skonfigurowanymi do zapobiegania wyciekom.', + description: 'Opis', + descriptionPlaceholder: 'Opisz zmienną', }, export: { title: 'Eksportować tajne zmienne środowiskowe?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} kroki do przodu', sessionStart: 'Początek sesji', currentState: 'Aktualny stan', - nodeTitleChange: 'Tytuł bloku zmieniony', - nodeDescriptionChange: 'Opis bloku zmieniony', - nodeDragStop: 'Blok przeniesiony', - nodeChange: 'Blok zmieniony', - nodeConnect: 'Blok połączony', - nodePaste: 'Blok wklejony', - nodeDelete: 'Blok usunięty', - nodeAdd: 'Blok dodany', - nodeResize: 'Notatka zmieniona', noteAdd: 'Notatka dodana', noteChange: 'Notatka zmieniona', noteDelete: 'Notatka usunięta', - edgeDelete: 'Blok rozłączony', + edgeDelete: 'Węzeł rozłączony', + nodeAdd: 'Węzeł dodany', + nodePaste: 'Węzeł wklejony', + nodeChange: 'Węzeł zmieniony', + nodeDelete: 'Węzeł usunięty', + nodeResize: 'Węzeł zmieniony rozmiar', + nodeConnect: 'Węzeł połączony', + nodeTitleChange: 'Tytuł węzła zmieniony', + nodeDescriptionChange: 'Opis węzła zmieniony', + nodeDragStop: 'Węzeł przeniesiony', }, errorMsg: { fieldRequired: '{{field}} jest wymagane', @@ -217,8 +219,6 @@ const translation = { loop: 'Pętla', }, tabs: { - 'searchBlock': 'Szukaj bloku', - 'blocks': 'Bloki', 'tools': 'Narzędzia', 'allTool': 'Wszystkie', 'builtInTool': 'Wbudowane', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Wyszukiwarka', 'agent': 'Strategia agenta', 'plugin': 'Wtyczka', + 'searchBlock': 'Wyszukaj węzeł', + 'blocks': 'Węzły', }, blocks: { 'start': 'Start', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Pole wprowadzania użytkownika', - changeBlock: 'Zmień blok', helpLink: 'Link do pomocy', about: 'O', createdBy: 'Stworzone przez ', nextStep: 'Następny krok', - addNextStep: 'Dodaj następny blok w tym przepływie pracy', - selectNextStep: 'Wybierz następny blok', runThisStep: 'Uruchom ten krok', checklist: 'Lista kontrolna', checklistTip: 'Upewnij się, że wszystkie problemy zostały rozwiązane przed opublikowaniem', checklistResolved: 'Wszystkie problemy zostały rozwiązane', - organizeBlocks: 'Organizuj bloki', change: 'Zmień', optional: '(opcjonalne)', moveToThisNode: 'Przenieś do tego węzła', + selectNextStep: 'Wybierz następny krok', + addNextStep: 'Dodaj następny krok w tym procesie roboczym', + changeBlock: 'Zmień węzeł', + organizeBlocks: 'Organizuj węzły', + minimize: 'Wyjdź z trybu pełnoekranowego', + maximize: 'Maksymalizuj płótno', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retryFailedTimes: '{{times}} ponawianie prób nie powiodło się', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'wymagane', @@ -535,6 +540,10 @@ const translation = { placeholder: 'Wklej tutaj ciąg cURL', title: 'Importowanie z cURL', }, + verifySSL: { + title: 'Zweryfikuj certyfikat SSL', + warningTooltip: 'Wyłączenie weryfikacji SSL nie jest zalecane w środowiskach produkcyjnych. Powinno to być używane tylko w rozwoju lub testowaniu, ponieważ naraża połączenie na zagrożenia bezpieczeństwa, takie jak ataki typu man-in-the-middle.', + }, }, code: { inputVars: 'Zmienne wejściowe', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Zmienne wejściowe', outputVars: { className: 'Nazwa klasy', + usage: 'Informacje o użyciu modelu', }, class: 'Klasa', classNamePlaceholder: 'Napisz nazwę swojej klasy', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Zmienna wejściowa', + outputVars: { + isSuccess: 'Czy się udało. W przypadku sukcesu wartość wynosi 1, w przypadku niepowodzenia wartość wynosi 0.', + errorReason: 'Powód błędu', + usage: 'Informacje o użyciu modelu', + }, extractParameters: 'Wyodrębnij parametry', importFromTool: 'Importuj z narzędzi', addExtractParameter: 'Dodaj parametr wyodrębniania', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Zaawansowane ustawienia', reasoningMode: 'Tryb wnioskowania', reasoningModeTip: 'Możesz wybrać odpowiedni tryb wnioskowania w zależności od zdolności modelu do reagowania na instrukcje dotyczące wywoływania funkcji lub zapytań.', - isSuccess: 'Czy się udało. W przypadku sukcesu wartość wynosi 1, w przypadku niepowodzenia wartość wynosi 0.', - errorReason: 'Powód błędu', }, iteration: { deleteTitle: 'Usunąć węzeł iteracji?', @@ -917,6 +930,35 @@ const translation = { deletionTip: 'Usunięcie jest nieodwracalne, proszę potwierdzić.', restorationTip: 'Po przywróceniu wersji bieżący szkic zostanie nadpisany.', }, + debug: { + noData: { + runThisNode: 'Uruchom ten węzeł', + description: 'Wyniki ostatniego uruchomienia będą wyświetlane tutaj', + }, + variableInspect: { + trigger: { + clear: 'Czysty', + running: 'Buforowanie statusu działania', + cached: 'Wyświetl zapisane zmienne', + stop: 'Zatrzymaj bieg', + normal: 'Inspekcja zmiennych', + }, + title: 'Inspekcja zmiennych', + chatNode: 'Rozmowa', + envNode: 'Środowisko', + systemNode: 'System', + edited: 'Edytowany', + clearAll: 'Resetuj wszystko', + emptyLink: 'Dowiedz się więcej', + clearNode: 'Wyczyść pamięć podręczną zmiennej', + reset: 'Zresetuj do ostatniej wartości run', + view: 'Zobacz dziennik', + resetConversationVar: 'Zresetuj zmienną rozmowy do wartości domyślnej', + emptyTip: 'Po przejściu przez węzeł na kanwie lub uruchomieniu węzła krok po kroku, możesz zobaczyć bieżącą wartość zmiennej węzła w Inspektorze Zmiennych.', + }, + settingsTab: 'Ustawienia', + lastRunTab: 'Ostatnie uruchomienie', + }, } export default translation diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index 64f7a85fe7..96d78dc9a3 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -334,6 +334,7 @@ const translation = { writeOpener: 'Escrever abertura', placeholder: 'Escreva sua mensagem de abertura aqui, você pode usar variáveis, tente digitar {{variável}}.', openingQuestion: 'Perguntas de Abertura', + openingQuestionPlaceholder: 'Você pode usar variáveis, tente digitar {{variable}}.', noDataPlaceHolder: 'Iniciar a conversa com o usuário pode ajudar a IA a estabelecer uma conexão mais próxima com eles em aplicativos de conversação.', varTip: 'Você pode usar variáveis, tente digitar {{variável}}', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 766053456a..099d0a33ea 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -93,6 +93,7 @@ const translation = { workflowShortDescription: 'Fluxo agêntico para automações inteligentes', noAppsFound: 'Nenhum aplicativo encontrado', advancedShortDescription: 'Fluxo aprimorado para conversas de múltiplos turnos', + dropDSLToCreateApp: 'Cole o arquivo DSL aqui para criar o aplicativo', }, editApp: 'Editar Informações', editAppTitle: 'Editar Informações do Aplicativo', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Configure o provedor para habilitar o rastreamento', moreProvider: 'Mais provedores', }, + arize: { + title: 'Arize', + description: 'Observabilidade de LLM de nível empresarial, avaliação online e offline, monitoramento e experimentação—impulsionada pelo OpenTelemetry. Projetado especificamente para aplicações baseadas em LLM e agentes.', + }, + phoenix: { + title: 'Phoenix', + description: 'Plataforma de observabilidade, avaliação, engenharia de prompts e experimentação de código aberto baseada em OpenTelemetry para seus fluxos de trabalho e agentes de LLM.', + }, langsmith: { title: 'LangSmith', description: 'Uma plataforma de desenvolvedor completa para cada etapa do ciclo de vida do aplicativo impulsionado por LLM.', @@ -163,6 +172,7 @@ const translation = { description: 'Weave é uma plataforma de código aberto para avaliar, testar e monitorar aplicações de LLM.', title: 'Trançar', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore', diff --git a/web/i18n/pt-BR/common.ts b/web/i18n/pt-BR/common.ts index d9409b5dd0..5bdf3388e1 100644 --- a/web/i18n/pt-BR/common.ts +++ b/web/i18n/pt-BR/common.ts @@ -58,6 +58,8 @@ const translation = { more: 'Mais', downloadSuccess: 'Download concluído.', format: 'Formato', + deSelectAll: 'Desmarcar tudo', + selectAll: 'Selecionar tudo', }, placeholder: { input: 'Por favor, insira', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'As extensões de API fornecem gerenciamento centralizado de API, simplificando a configuração para uso fácil em todos os aplicativos da Dify.', link: 'Saiba como desenvolver sua própria Extensão de API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Adicionar Extensão de API', selector: { title: 'Extensão de API', diff --git a/web/i18n/pt-BR/dataset-creation.ts b/web/i18n/pt-BR/dataset-creation.ts index 9023d1a6dc..db3b5af302 100644 --- a/web/i18n/pt-BR/dataset-creation.ts +++ b/web/i18n/pt-BR/dataset-creation.ts @@ -58,7 +58,6 @@ const translation = { crawlSubPage: 'Rastrear subpáginas', selectAll: 'Selecionar tudo', resetAll: 'Redefinir tudo', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', includeOnlyPaths: 'Incluir apenas caminhos', configure: 'Configurar', limit: 'Limite', @@ -87,7 +86,6 @@ const translation = { configureJinaReader: 'Configurar o Leitor Jina', waterCrawlNotConfigured: 'Watercrawl não está configurado', waterCrawlNotConfiguredDescription: 'Configure o Watercrawl com a chave da API para usá-lo.', - watercrawlDocLink: 'https://docs.dify.ai/pt/guias/base-de-conhecimentos/criar-conhecimento-e-enviar-documentos/importar-dados-de-conteudo/sincronizar-a-partir-do-site', watercrawlDoc: 'Documentos do Watercrawl', configureWatercrawl: 'Configurar Watercrawl', }, diff --git a/web/i18n/pt-BR/dataset-documents.ts b/web/i18n/pt-BR/dataset-documents.ts index 9a3d13bcab..462a3c09ef 100644 --- a/web/i18n/pt-BR/dataset-documents.ts +++ b/web/i18n/pt-BR/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Excluir', enableWarning: 'O arquivo arquivado não pode ser habilitado', sync: 'Sincronizar', + resume: 'Retomar', + pause: 'Pausa', }, index: { enable: 'Habilitar', @@ -389,6 +391,8 @@ const translation = { newChildChunk: 'Novo pedaço filho', characters_one: 'personagem', parentChunk: 'Pedaço pai', + keywordEmpty: 'A palavra-chave não pode estar vazia', + keywordDuplicate: 'A palavra-chave já existe', }, } diff --git a/web/i18n/pt-BR/dataset.ts b/web/i18n/pt-BR/dataset.ts index 7d5f75aae6..53b13b7567 100644 --- a/web/i18n/pt-BR/dataset.ts +++ b/web/i18n/pt-BR/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'O nome dos metadados não pode estar vazio', invalid: 'O nome de metadata só pode conter letras minúsculas, números e sublinhados e deve começar com uma letra minúscula.', + tooLong: 'O nome dos metadados não pode exceder {{max}} caracteres.', }, batchEditMetadata: { editDocumentsNum: 'Editando {{num}} documentos', diff --git a/web/i18n/pt-BR/plugin.ts b/web/i18n/pt-BR/plugin.ts index 8eb44f83d6..be8e7e7f97 100644 --- a/web/i18n/pt-BR/plugin.ts +++ b/web/i18n/pt-BR/plugin.ts @@ -47,14 +47,14 @@ const translation = { toolSelector: { uninstalledLink: 'Gerenciar em plug-ins', unsupportedContent2: 'Clique para mudar de versão.', - auto: 'Automático', + auto: 'Auto', title: 'Adicionar ferramenta', params: 'CONFIGURAÇÃO DE RACIOCÍNIO', toolLabel: 'Ferramenta', paramsTip1: 'Controla os parâmetros de inferência do LLM.', descriptionLabel: 'Descrição da ferramenta', uninstalledContent: 'Este plug-in é instalado a partir do repositório local/GitHub. Por favor, use após a instalação.', - paramsTip2: 'Quando \'Automático\' está desativado, o valor padrão é usado.', + paramsTip2: 'Quando \'Auto\' está desativado, o valor padrão é usado.', placeholder: 'Selecione uma ferramenta...', empty: 'Clique no botão \'+\' para adicionar ferramentas. Você pode adicionar várias ferramentas.', settings: 'CONFIGURAÇÕES DO USUÁRIO', @@ -137,6 +137,7 @@ const translation = { installing: 'Instalar...', uploadingPackage: 'Carregando {{packageName}} ...', dropPluginToInstall: 'Solte o pacote de plug-in aqui para instalar', + installWarning: 'Este plugin não é permitido ser instalado.', }, installFromGitHub: { selectVersionPlaceholder: 'Selecione uma versão', @@ -172,7 +173,7 @@ const translation = { recentlyUpdated: 'Atualizado recentemente', newlyReleased: 'Recém-lançado', }, - sortBy: 'Cidade negra', + sortBy: 'Ordenar por', viewMore: 'Ver mais', and: 'e', pluginsResult: '{{num}} resultados', @@ -194,7 +195,6 @@ const translation = { }, installAction: 'Instalar', endpointsEnabled: '{{num}} conjuntos de endpoints habilitados', - submitPlugin: 'Enviar plugin', searchPlugins: 'Pesquisar plugins', searchInMarketplace: 'Pesquisar no Marketplace', installPlugin: 'Instale o plugin', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'A versão atual do Dify não é compatível com este plugin, por favor atualize para a versão mínima exigida: {{minimalDifyVersion}}', requestAPlugin: 'Solicitar um plugin', + publishPlugins: 'Publicar plugins', } export default translation diff --git a/web/i18n/pt-BR/tools.ts b/web/i18n/pt-BR/tools.ts index dde7add80a..bd57de362f 100644 --- a/web/i18n/pt-BR/tools.ts +++ b/web/i18n/pt-BR/tools.ts @@ -139,19 +139,95 @@ const translation = { addToolModal: { category: 'categoria', type: 'tipo', - emptyTip: 'Vá para "Fluxo de trabalho - > Publicar como ferramenta"', add: 'adicionar', - emptyTitle: 'Nenhuma ferramenta de fluxo de trabalho disponível', added: 'Adicionado', manageInTools: 'Gerenciar em Ferramentas', - emptyTitleCustom: 'Nenhuma ferramenta personalizada disponível', - emptyTipCustom: 'Criar uma ferramenta personalizada', + custom: { + title: 'Nenhuma ferramenta personalizada disponível', + tip: 'Crie uma ferramenta personalizada', + }, + workflow: { + title: 'Nenhuma ferramenta de fluxo de trabalho disponível', + tip: 'Publique fluxos de trabalho como ferramentas no Studio', + }, + mcp: { + title: 'Nenhuma ferramenta MCP disponível', + tip: 'Adicionar um servidor MCP', + }, + agent: { + title: 'Nenhuma estratégia de agente disponível', + }, }, openInStudio: 'Abrir no Studio', customToolTip: 'Saiba mais sobre as ferramentas personalizadas da Dify', toolNameUsageTip: 'Nome da chamada da ferramenta para raciocínio e solicitação do agente', copyToolName: 'Nome da cópia', noTools: 'Nenhuma ferramenta encontrada', + mcp: { + create: { + cardTitle: 'Adicionar Servidor MCP (HTTP)', + cardLink: 'Saiba mais sobre a integração do servidor MCP', + }, + noConfigured: 'Servidor Não Configurado', + updateTime: 'Atualizado', + toolsCount: '{{count}} ferramentas', + noTools: 'Nenhuma ferramenta disponível', + modal: { + title: 'Adicionar Servidor MCP (HTTP)', + editTitle: 'Editar Servidor MCP (HTTP)', + name: 'Nome & Ícone', + namePlaceholder: 'Dê um nome ao seu servidor MCP', + serverUrl: 'URL do Servidor', + serverUrlPlaceholder: 'URL para o endpoint do servidor', + serverUrlWarning: 'Atualizar o endereço do servidor pode interromper aplicações que dependem deste servidor', + serverIdentifier: 'Identificador do Servidor', + serverIdentifierTip: 'Identificador único para o servidor MCP dentro do espaço de trabalho. Apenas letras minúsculas, números, sublinhados e hífens. Até 24 caracteres.', + serverIdentifierPlaceholder: 'Identificador único, ex: meu-servidor-mcp', + serverIdentifierWarning: 'O servidor não será reconhecido por aplicativos existentes após uma mudança de ID', + cancel: 'Cancelar', + save: 'Salvar', + confirm: 'Adicionar e Autorizar', + }, + delete: 'Remover Servidor MCP', + deleteConfirmTitle: 'Você gostaria de remover {{mcp}}?', + operation: { + edit: 'Editar', + remove: 'Remover', + }, + authorize: 'Autorizar', + authorizing: 'Autorizando...', + authorizingRequired: 'Autorização é necessária', + authorizeTip: 'Após a autorização, as ferramentas serão exibidas aqui.', + update: 'Atualizar', + updating: 'Atualizando', + gettingTools: 'Obtendo Ferramentas...', + updateTools: 'Atualizando Ferramentas...', + toolsEmpty: 'Ferramentas não carregadas', + getTools: 'Obter ferramentas', + toolUpdateConfirmTitle: 'Atualizar Lista de Ferramentas', + toolUpdateConfirmContent: 'Atualizar a lista de ferramentas pode afetar aplicativos existentes. Você deseja continuar?', + toolsNum: '{{count}} ferramentas incluídas', + onlyTool: '1 ferramenta incluída', + identifier: 'Identificador do Servidor (Clique para Copiar)', + server: { + title: 'Servidor MCP', + url: 'URL do Servidor', + reGen: 'Você deseja regenerar a URL do servidor?', + addDescription: 'Adicionar descrição', + edit: 'Editar descrição', + modal: { + addTitle: 'Adicionar descrição para habilitar o servidor MCP', + editTitle: 'Editar descrição', + description: 'Descrição', + descriptionPlaceholder: 'Explique o que esta ferramenta faz e como deve ser utilizada pelo LLM', + parameters: 'Parâmetros', + parametersTip: 'Adicione descrições para cada parâmetro para ajudar o LLM a entender seus propósitos e restrições.', + parametersPlaceholder: 'Propósito e restrições do parâmetro', + confirm: 'Habilitar Servidor MCP', + }, + publishTip: 'Aplicativo não publicado. Por favor, publique o aplicativo primeiro.', + }, + }, } export default translation diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 11ee0ed9a1..1cb323f59a 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Definir valor da variável', needConnectTip: 'Este passo não está conectado a nada', maxTreeDepth: 'Limite máximo de {{depth}} nós por ramo', - needEndNode: 'O bloco de fim deve ser adicionado', - needAnswerNode: 'O bloco de resposta deve ser adicionado', workflowProcess: 'Processo de fluxo de trabalho', notRunning: 'Ainda não está em execução', previewPlaceholder: 'Digite o conteúdo na caixa abaixo para começar a depurar o Chatbot', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Saiba mais', copy: 'Copiar', duplicate: 'Duplicar', - addBlock: 'Adicionar bloco', pasteHere: 'Colar aqui', pointerMode: 'Modo ponteiro', handMode: 'Modo mão', @@ -115,6 +112,9 @@ const translation = { exitVersions: 'Versões de Sair', exportSVG: 'Exportar como SVG', exportJPEG: 'Exportar como JPEG', + addBlock: 'Adicionar Nó', + needEndNode: 'O nó de Fim deve ser adicionado', + needAnswerNode: 'O nó de resposta deve ser adicionado', }, env: { envPanelTitle: 'Variáveis de Ambiente', @@ -129,6 +129,8 @@ const translation = { value: 'Valor', valuePlaceholder: 'valor da env', secretTip: 'Usado para definir informações ou dados sensíveis, com configurações DSL configuradas para prevenção de vazamentos.', + description: 'Descrição', + descriptionPlaceholder: 'Descreva a variável', }, export: { title: 'Exportar variáveis de ambiente secretas?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} passos para frente', sessionStart: 'Início da sessão', currentState: 'Estado atual', - nodeTitleChange: 'Título do bloco alterado', - nodeDescriptionChange: 'Descrição do bloco alterada', - nodeDragStop: 'Bloco movido', - nodeChange: 'Bloco alterado', - nodeConnect: 'Bloco conectado', - nodePaste: 'Bloco colado', - nodeDelete: 'Bloco excluído', - nodeAdd: 'Bloco adicionado', - nodeResize: 'Nota redimensionada', noteAdd: 'Nota adicionada', noteChange: 'Nota alterada', noteDelete: 'Conexão excluída', - edgeDelete: 'Bloco desconectado', + nodeConnect: 'Nó conectado', + nodeDelete: 'Nó deletado', + nodePaste: 'Nó colado', + nodeTitleChange: 'Título do nó alterado', + nodeAdd: 'Nó adicionado', + nodeDescriptionChange: 'Descrição do nó alterada', + edgeDelete: 'Nó desconectado', + nodeResize: 'Nó redimensionado', + nodeChange: 'Nó alterado', + nodeDragStop: 'Nó movido', }, errorMsg: { fieldRequired: '{{field}} é obrigatório', @@ -217,8 +219,6 @@ const translation = { loop: 'Laço', }, tabs: { - 'searchBlock': 'Buscar bloco', - 'blocks': 'Blocos', 'tools': 'Ferramentas', 'allTool': 'Todos', 'builtInTool': 'Integrado', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Ferramenta de pesquisa', 'plugin': 'Plug-in', 'agent': 'Estratégia do agente', + 'blocks': 'Nodos', + 'searchBlock': 'Nó de busca', }, blocks: { 'start': 'Iniciar', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Campo de entrada do usuário', - changeBlock: 'Mudar bloco', helpLink: 'Link de ajuda', about: 'Sobre', createdBy: 'Criado por ', nextStep: 'Próximo passo', - addNextStep: 'Adicionar o próximo bloco neste fluxo de trabalho', - selectNextStep: 'Selecionar próximo bloco', runThisStep: 'Executar este passo', checklist: 'Lista de verificação', checklistTip: 'Certifique-se de que todos os problemas foram resolvidos antes de publicar', checklistResolved: 'Todos os problemas foram resolvidos', - organizeBlocks: 'Organizar blocos', change: 'Mudar', optional: '(opcional)', moveToThisNode: 'Mova-se para este nó', + changeBlock: 'Mudar Nó', + addNextStep: 'Adicione o próximo passo neste fluxo de trabalho', + organizeBlocks: 'Organizar nós', + selectNextStep: 'Selecione o próximo passo', + maximize: 'Maximize Canvas', + minimize: 'Sair do Modo Tela Cheia', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { ms: 'ms', retries: '{{num}} Tentativas', }, + typeSwitch: {}, }, start: { required: 'requerido', @@ -535,6 +540,10 @@ const translation = { placeholder: 'Cole a string cURL aqui', title: 'Importar do cURL', }, + verifySSL: { + title: 'Verificar o certificado SSL', + warningTooltip: 'Desabilitar a verificação SSL não é recomendado para ambientes de produção. Isso deve ser usado apenas em desenvolvimento ou teste, pois torna a conexão vulnerável a ameaças de segurança, como ataques man-in-the-middle.', + }, }, code: { inputVars: 'Variáveis de entrada', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Variáveis de entrada', outputVars: { className: 'Nome da classe', + usage: 'Informações de uso do modelo', }, class: 'Classe', classNamePlaceholder: 'Escreva o nome da sua classe', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Variável de entrada', + outputVars: { + isSuccess: 'É sucesso. Em caso de sucesso, o valor é 1, em caso de falha, o valor é 0.', + errorReason: 'Motivo do erro', + usage: 'Informações de uso do modelo', + }, extractParameters: 'Extrair parâmetros', importFromTool: 'Importar das ferramentas', addExtractParameter: 'Adicionar parâmetro de extração', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Configuração avançada', reasoningMode: 'Modo de raciocínio', reasoningModeTip: 'Você pode escolher o modo de raciocínio apropriado com base na capacidade do modelo de responder a instruções para chamadas de função ou prompts.', - isSuccess: 'É sucesso. Em caso de sucesso, o valor é 1, em caso de falha, o valor é 0.', - errorReason: 'Motivo do erro', }, iteration: { deleteTitle: 'Excluir nó de iteração?', @@ -917,6 +930,35 @@ const translation = { currentDraft: 'Rascunho Atual', deletionTip: 'A exclusão é irreversível, por favor confirme.', }, + debug: { + noData: { + runThisNode: 'Execute este nó', + description: 'Os resultados da última execução serão exibidos aqui', + }, + variableInspect: { + trigger: { + normal: 'Inspecionar Variável', + stop: 'Pare de correr', + clear: 'Claro', + running: 'Status de execução do cache', + cached: 'Ver variáveis em cache', + }, + systemNode: 'Sistema', + edited: 'Editado', + clearAll: 'Redefinir tudo', + clearNode: 'Limpar variável em cache', + emptyLink: 'Saiba mais', + chatNode: 'Conversa', + envNode: 'Ambiente', + title: 'Inspecionar Variável', + reset: 'Redefinir para o último valor de execução', + resetConversationVar: 'Redefinir a variável da conversa para o valor padrão', + view: 'Ver log', + emptyTip: 'Após passar por um nó na tela ou executar um nó passo a passo, você pode visualizar o valor atual da variável do nó na Inspecção de Variáveis.', + }, + settingsTab: 'Configurações', + lastRunTab: 'Última execução', + }, } export default translation diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index cd267e7b66..2d63bdfba7 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -93,6 +93,7 @@ const translation = { noTemplateFound: 'Nu s-au găsit șabloane', forAdvanced: 'PENTRU UTILIZATORII AVANSAȚI', chooseAppType: 'Alegeți un tip de aplicație', + dropDSLToCreateApp: 'Trageți fișierul DSL aici pentru a crea aplicația', }, editApp: 'Editează Info', editAppTitle: 'Editează Info Aplicație', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Configurați furnizorul pentru a activa urmărirea', moreProvider: 'Mai mulți furnizori', }, + arize: { + title: 'Arize', + description: 'Observabilitate LLM de nivel enterprise, evaluare online și offline, monitorizare și experimentare—alimentată de OpenTelemetry. Proiectată special pentru aplicații bazate pe LLM și agenți.', + }, + phoenix: { + title: 'Phoenix', + description: 'Platformă open-source și bazată pe OpenTelemetry pentru observabilitate, evaluare, inginerie de prompturi și experimentare pentru fluxurile de lucru și agenții LLM.', + }, langsmith: { title: 'LangSmith', description: 'O platformă de dezvoltare all-in-one pentru fiecare etapă a ciclului de viață al aplicației bazate pe LLM.', @@ -163,6 +172,9 @@ const translation = { title: 'Împletește', description: 'Weave este o platformă open-source pentru evaluarea, testarea și monitorizarea aplicațiilor LLM.', }, + aliyun: { + description: 'Platforma de observabilitate SaaS oferită de Alibaba Cloud permite monitorizarea, urmărirea și evaluarea aplicațiilor Dify din cutie.', + }, }, answerIcon: { descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore', diff --git a/web/i18n/ro-RO/common.ts b/web/i18n/ro-RO/common.ts index f736240a78..168d201e0d 100644 --- a/web/i18n/ro-RO/common.ts +++ b/web/i18n/ro-RO/common.ts @@ -58,6 +58,8 @@ const translation = { format: 'Format', downloadSuccess: 'Descărcarea a fost finalizată.', more: 'Mai mult', + deSelectAll: 'Deselectați tot', + selectAll: 'Selectați tot', }, placeholder: { input: 'Vă rugăm să introduceți', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'Extensiile bazate pe API oferă o gestionare centralizată a API-urilor, simplificând configurația pentru o utilizare ușoară în aplicațiile Dify.', link: 'Aflați cum să dezvoltați propria extensie bazată pe API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Adăugați extensie API', selector: { title: 'Extensie API', diff --git a/web/i18n/ro-RO/dataset-creation.ts b/web/i18n/ro-RO/dataset-creation.ts index ce6872c654..77c3bce88a 100644 --- a/web/i18n/ro-RO/dataset-creation.ts +++ b/web/i18n/ro-RO/dataset-creation.ts @@ -65,7 +65,6 @@ const translation = { firecrawlTitle: 'Extrageți conținut web cu 🔥Firecrawl', unknownError: 'Eroare necunoscută', scrapTimeInfo: 'Pagini răzuite {{total}} în total în {{timp}}s', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', excludePaths: 'Excluderea căilor', resetAll: 'Resetați toate', extractOnlyMainContent: 'Extrageți doar conținutul principal (fără anteturi, navigări, subsoluri etc.)', @@ -86,7 +85,6 @@ const translation = { watercrawlTitle: 'Extrageți conținut web cu Watercrawl', configureJinaReader: 'Configurează Jina Reader', waterCrawlNotConfiguredDescription: 'Configurează Watercrawl cu cheia API pentru a-l folosi.', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureFirecrawl: 'Configurează Firecrawl', watercrawlDoc: 'Documentele Watercrawl', configureWatercrawl: 'Configurează Watercrawl', diff --git a/web/i18n/ro-RO/dataset-documents.ts b/web/i18n/ro-RO/dataset-documents.ts index e42be87502..876ddde40b 100644 --- a/web/i18n/ro-RO/dataset-documents.ts +++ b/web/i18n/ro-RO/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Șterge', enableWarning: 'Fișierul arhivat nu poate fi activat', sync: 'Sincronizează', + pause: 'Pauză', + resume: 'Reia', }, index: { enable: 'Activează', @@ -389,6 +391,8 @@ const translation = { regeneratingTitle: 'Regenerarea bucăților secundare', addChildChunk: 'Adăugați o bucată copil', searchResults_other: 'REZULTATELE', + keywordDuplicate: 'Cuvântul cheie există deja', + keywordEmpty: 'Cuvântul cheie nu poate fi gol', }, } diff --git a/web/i18n/ro-RO/dataset.ts b/web/i18n/ro-RO/dataset.ts index dd7b2d6900..8305029a97 100644 --- a/web/i18n/ro-RO/dataset.ts +++ b/web/i18n/ro-RO/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { invalid: 'Numele metadatelor poate conține doar litere mici, cifre și underscore și trebuie să înceapă cu o literă mică.', empty: 'Numele metadatelor nu poate fi gol', + tooLong: 'Numele metadatelor nu poate depăși {{max}} caractere', }, batchEditMetadata: { multipleValue: 'Valoare multiplă', diff --git a/web/i18n/ro-RO/plugin.ts b/web/i18n/ro-RO/plugin.ts index 11dc6b8f59..1c7d173f8f 100644 --- a/web/i18n/ro-RO/plugin.ts +++ b/web/i18n/ro-RO/plugin.ts @@ -46,7 +46,7 @@ const translation = { }, toolSelector: { unsupportedContent: 'Versiunea de plugin instalată nu oferă această acțiune.', - auto: 'Automat', + auto: 'Auto', empty: 'Faceți clic pe butonul "+" pentru a adăuga instrumente. Puteți adăuga mai multe instrumente.', uninstalledContent: 'Acest plugin este instalat din depozitul local/GitHub. Vă rugăm să utilizați după instalare.', descriptionLabel: 'Descrierea instrumentului', @@ -54,7 +54,7 @@ const translation = { uninstalledLink: 'Gestionați în pluginuri', paramsTip1: 'Controlează parametrii de inferență LLM.', params: 'CONFIGURAREA RAȚIONAMENTULUI', - paramsTip2: 'Când "Automat" este dezactivat, se folosește valoarea implicită.', + paramsTip2: 'Când "Auto" este dezactivat, se folosește valoarea implicită.', settings: 'SETĂRI UTILIZATOR', unsupportedTitle: 'Acțiune neacceptată', placeholder: 'Selectați un instrument...', @@ -137,6 +137,7 @@ const translation = { pluginLoadErrorDesc: 'Acest plugin nu va fi instalat', installedSuccessfullyDesc: 'Pluginul a fost instalat cu succes.', readyToInstall: 'Despre instalarea următorului plugin', + installWarning: 'Acest plugin nu este permis să fie instalat.', }, installFromGitHub: { installFailed: 'Instalarea a eșuat', @@ -173,7 +174,7 @@ const translation = { firstReleased: 'Prima lansare', }, noPluginFound: 'Nu s-a găsit niciun plugin', - sortBy: 'Orașul negru', + sortBy: 'Sortează după', discover: 'Descoperi', empower: 'Îmbunătățește-ți dezvoltarea AI', pluginsResult: '{{num}} rezultate', @@ -192,7 +193,6 @@ const translation = { installingWithSuccess: 'Instalarea pluginurilor {{installingLength}}, {{successLength}} succes.', installing: 'Instalarea pluginurilor {{installingLength}}, 0 terminat.', }, - submitPlugin: 'Trimite plugin', fromMarketplace: 'Din Marketplace', from: 'Din', findMoreInMarketplace: 'Află mai multe în Marketplace', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Versiunea curentă Dify nu este compatibilă cu acest plugin, vă rugăm să faceți upgrade la versiunea minimă necesară: {{minimalDifyVersion}}', requestAPlugin: 'Solicitați un plugin', + publishPlugins: 'Publicați pluginuri', } export default translation diff --git a/web/i18n/ro-RO/tools.ts b/web/i18n/ro-RO/tools.ts index 44530754e3..8d8c77a911 100644 --- a/web/i18n/ro-RO/tools.ts +++ b/web/i18n/ro-RO/tools.ts @@ -142,16 +142,92 @@ const translation = { manageInTools: 'Gestionați în Instrumente', add: 'adăuga', type: 'tip', - emptyTitle: 'Nu este disponibil niciun instrument de flux de lucru', - emptyTip: 'Accesați "Flux de lucru -> Publicați ca instrument"', - emptyTitleCustom: 'Nu este disponibil niciun instrument personalizat', - emptyTipCustom: 'Crearea unui instrument personalizat', + custom: { + title: 'Niciun instrument personalizat disponibil', + tip: 'Creează un instrument personalizat', + }, + workflow: { + title: 'Niciun instrument de flux de lucru disponibil', + tip: 'Publicați fluxuri de lucru ca instrumente în Studio', + }, + mcp: { + title: 'Niciun instrument MCP disponibil', + tip: 'Adăugați un server MCP', + }, + agent: { + title: 'Nicio strategie de agent disponibilă', + }, }, openInStudio: 'Deschide în Studio', customToolTip: 'Aflați mai multe despre instrumentele personalizate Dify', toolNameUsageTip: 'Numele de apel al instrumentului pentru raționamentul și solicitarea agentului', copyToolName: 'Copiază numele', noTools: 'Nu s-au găsit unelte', + mcp: { + create: { + cardTitle: 'Adăugare Server MCP (HTTP)', + cardLink: 'Aflați mai multe despre integrarea serverului MCP', + }, + noConfigured: 'Server Neconfigurat', + updateTime: 'Actualizat', + toolsCount: '{count} unelte', + noTools: 'Nu există unelte disponibile', + modal: { + title: 'Adăugare Server MCP (HTTP)', + editTitle: 'Editare Server MCP (HTTP)', + name: 'Nume și Pictogramă', + namePlaceholder: 'Denumiți-vă serverul MCP', + serverUrl: 'URL Server', + serverUrlPlaceholder: 'URL către endpoint-ul serverului', + serverUrlWarning: 'Actualizarea adresei serverului poate întrerupe aplicațiile care depind de acesta', + serverIdentifier: 'Identificator Server', + serverIdentifierTip: 'Identificator unic pentru serverul MCP în spațiul de lucru. Doar litere mici, cifre, underscore și cratime. Maxim 24 de caractere.', + serverIdentifierPlaceholder: 'Identificator unic, ex: my-mcp-server', + serverIdentifierWarning: 'Serverul nu va fi recunoscut de aplicațiile existente după schimbarea ID-ului', + cancel: 'Anulare', + save: 'Salvare', + confirm: 'Adăugare și Autorizare', + }, + delete: 'Eliminare Server MCP', + deleteConfirmTitle: 'Ștergeți {mcp}?', + operation: { + edit: 'Editare', + remove: 'Eliminare', + }, + authorize: 'Autorizare', + authorizing: 'Se autorizează...', + authorizingRequired: 'Autorizare necesară', + authorizeTip: 'După autorizare, uneltele vor fi afișate aici.', + update: 'Actualizare', + updating: 'Se actualizează...', + gettingTools: 'Se obțin unelte...', + updateTools: 'Se actualizează unelte...', + toolsEmpty: 'Unelte neîncărcate', + getTools: 'Obține unelte', + toolUpdateConfirmTitle: 'Actualizare Listă Unelte', + toolUpdateConfirmContent: 'Actualizarea listei de unelte poate afecta aplicațiile existente. Continuați?', + toolsNum: '{count} unelte incluse', + onlyTool: '1 unealtă inclusă', + identifier: 'Identificator Server (Clic pentru Copiere)', + server: { + title: 'Server MCP', + url: 'URL Server', + reGen: 'Regenerați URL server?', + addDescription: 'Adăugare descriere', + edit: 'Editare descriere', + modal: { + addTitle: 'Adăugați descriere pentru activarea serverului MCP', + editTitle: 'Editare descriere', + description: 'Descriere', + descriptionPlaceholder: 'Explicați funcționalitatea acestei unelte și cum ar trebui să fie utilizată de LLM', + parameters: 'Parametri', + parametersTip: 'Adăugați descrieri pentru fiecare parametru pentru a ajuta LLM să înțeleagă scopul și constrângerile.', + parametersPlaceholder: 'Scopul și constrângerile parametrului', + confirm: 'Activare Server MCP', + }, + publishTip: 'Aplicație nepublicată. Publicați aplicația mai întâi.', + }, + }, } export default translation diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index fabea57556..886dc3b790 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Setează valoarea variabilei', needConnectTip: 'Acest pas nu este conectat la nimic', maxTreeDepth: 'Limită maximă de {{depth}} noduri pe ramură', - needEndNode: 'Trebuie adăugat blocul de sfârșit', - needAnswerNode: 'Trebuie adăugat blocul de răspuns', workflowProcess: 'Proces de flux de lucru', notRunning: 'Încă nu rulează', previewPlaceholder: 'Introduceți conținutul în caseta de mai jos pentru a începe depanarea Chatbotului', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Află mai multe', copy: 'Copiază', duplicate: 'Duplică', - addBlock: 'Adaugă bloc', pasteHere: 'Lipește aici', pointerMode: 'Modul pointer', handMode: 'Modul mână', @@ -115,6 +112,9 @@ const translation = { publishUpdate: 'Publicați actualizarea', referenceVar: 'Variabilă de referință', exportJPEG: 'Exportă ca JPEG', + addBlock: 'Adaugă nod', + needAnswerNode: 'Nodul de răspuns trebuie adăugat', + needEndNode: 'Nodul de sfârșit trebuie adăugat', }, env: { envPanelTitle: 'Variabile de Mediu', @@ -129,6 +129,8 @@ const translation = { value: 'Valoare', valuePlaceholder: 'valoare mediu', secretTip: 'Utilizat pentru a defini informații sau date sensibile, cu setări DSL configurate pentru prevenirea scurgerilor.', + description: 'Descriere', + descriptionPlaceholder: 'Descrieți variabila', }, export: { title: 'Exportă variabile de mediu secrete?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} pași înainte', sessionStart: 'Începutul sesiuni', currentState: 'Stare actuală', - nodeTitleChange: 'Titlul blocului a fost schimbat', - nodeDescriptionChange: 'Descrierea blocului a fost schimbată', - nodeDragStop: 'Bloc mutat', - nodeChange: 'Bloc schimbat', - nodeConnect: 'Bloc conectat', - nodePaste: 'Bloc lipit', - nodeDelete: 'Bloc șters', - nodeAdd: 'Bloc adăugat', - nodeResize: 'Bloc redimensionat', noteAdd: 'Notă adăugată', noteChange: 'Notă modificată', noteDelete: 'Notă ștearsă', - edgeDelete: 'Bloc deconectat', + nodeResize: 'Nod redimensionat', + nodeConnect: 'Nod conectat', + nodeTitleChange: 'Titlul nodului a fost schimbat', + nodeChange: 'Nodul s-a schimbat', + nodePaste: 'Node lipit', + nodeDelete: 'Nod șters', + nodeDescriptionChange: 'Descrierea nodului a fost modificată', + edgeDelete: 'Nod deconectat', + nodeAdd: 'Nod adăugat', + nodeDragStop: 'Nod mutat', }, errorMsg: { fieldRequired: '{{field}} este obligatoriu', @@ -217,8 +219,6 @@ const translation = { loop: 'Loop', }, tabs: { - 'searchBlock': 'Caută bloc', - 'blocks': 'Blocuri', 'tools': 'Instrumente', 'allTool': 'Toate', 'builtInTool': 'Integrat', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Instrument de căutare', 'agent': 'Strategia agentului', 'plugin': 'Plugin', + 'blocks': 'Noduri', + 'searchBlock': 'Căutare nod', }, blocks: { 'start': 'Începe', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Câmp de introducere utilizator', - changeBlock: 'Schimbă blocul', helpLink: 'Link de ajutor', about: 'Despre', createdBy: 'Creat de ', nextStep: 'Pasul următor', - addNextStep: 'Adăugați următorul bloc în acest flux de lucru', - selectNextStep: 'Selectați următorul bloc', runThisStep: 'Rulează acest pas', checklist: 'Lista de verificare', checklistTip: 'Asigurați-vă că toate problemele sunt rezolvate înainte de publicare', checklistResolved: 'Toate problemele au fost rezolvate', - organizeBlocks: 'Organizează blocurile', change: 'Schimbă', optional: '(opțional)', moveToThisNode: 'Mutați la acest nod', + organizeBlocks: 'Organizează nodurile', + addNextStep: 'Adăugați următorul pas în acest flux de lucru', + changeBlock: 'Schimbă nodul', + selectNextStep: 'Selectați Pasul Următor', + maximize: 'Maximize Canvas', + minimize: 'Iesi din modul pe tot ecranul', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retries: '{{num}} Încercări', retryTimes: 'Reîncercați {{times}} ori în caz de eșec', }, + typeSwitch: {}, }, start: { required: 'necesar', @@ -535,6 +540,10 @@ const translation = { placeholder: 'Lipiți șirul cURL aici', title: 'Importați din cURL', }, + verifySSL: { + title: 'Verifică certificatul SSL', + warningTooltip: 'Dezactivarea verificării SSL nu este recomandată pentru medii de producție. Acest lucru ar trebui să fie folosit doar în dezvoltare sau testare, deoarece face conexiunea vulnerabilă la amenințări de securitate, cum ar fi atacurile man-in-the-middle.', + }, }, code: { inputVars: 'Variabile de intrare', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Variabile de intrare', outputVars: { className: 'Nume clasă', + usage: 'Informații de utilizare a modelului', }, class: 'Clasă', classNamePlaceholder: 'Scrieți numele clasei', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Variabilă de intrare', + outputVars: { + isSuccess: 'Este succes. În caz de succes valoarea este 1, în caz de eșec valoarea este 0.', + errorReason: 'Motivul erorii', + usage: 'Informații de utilizare a modelului', + }, extractParameters: 'Extrageți parametrii', importFromTool: 'Importă din instrumente', addExtractParameter: 'Adăugați parametru de extragere', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Setare avansată', reasoningMode: 'Mod de raționament', reasoningModeTip: 'Puteți alege modul de raționament potrivit în funcție de capacitatea modelului de a răspunde la instrucțiuni pentru apelarea funcțiilor sau prompturi.', - isSuccess: 'Este succes. În caz de succes valoarea este 1, în caz de eșec valoarea este 0.', - errorReason: 'Motivul erorii', }, iteration: { deleteTitle: 'Ștergeți nodul de iterație?', @@ -917,6 +930,35 @@ const translation = { deletionTip: 'Ștergerea este irreversibilă, vă rugăm să confirmați.', currentDraft: 'Draftul curent', }, + debug: { + noData: { + runThisNode: 'Rulează acest nod', + description: 'Rezultatele ultimei rulări vor fi afișate aici', + }, + variableInspect: { + trigger: { + clear: 'Clar', + running: 'Starea de funcționare a cache-ului', + cached: 'Vizualizează variabilele cached', + normal: 'Inspectare variabilă', + stop: 'Oprește-te din alergat', + }, + chatNode: 'Conversație', + title: 'Inspectare variabilă', + systemNode: 'Sistem', + clearAll: 'Resetare toate', + emptyLink: 'Învățați mai multe', + view: 'Vizualizați jurnalul', + envNode: 'Mediu', + reset: 'Resetează la ultima valoare rulată', + resetConversationVar: 'Resetați variabila de conversație la valoarea implicită', + edited: 'Editat', + clearNode: 'Șterge variabila cached', + emptyTip: 'După ce ai trecut printr-un nod pe canvas sau ai rulat un nod pas cu pas, poți vizualiza valoarea curentă a variabilei nodului în Inspectarea Variabilelor.', + }, + settingsTab: 'Setări', + lastRunTab: 'Ultima execuție', + }, } export default translation diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 00cd6e8a75..5d4dbb53d3 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -370,6 +370,7 @@ const translation = { writeOpener: 'Написать начальное сообщение', placeholder: 'Напишите здесь свое начальное сообщение, вы можете использовать переменные, попробуйте ввести {{variable}}.', openingQuestion: 'Начальные вопросы', + openingQuestionPlaceholder: 'Вы можете использовать переменные, попробуйте ввести {{variable}}.', noDataPlaceHolder: 'Начало разговора с пользователем может помочь ИИ установить более тесную связь с ним в диалоговых приложениях.', varTip: 'Вы можете использовать переменные, попробуйте ввести {{variable}}', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index 428d4c4e57..de8047b080 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -97,6 +97,7 @@ const translation = { noTemplateFoundTip: 'Попробуйте искать по разным ключевым словам.', completionUserDescription: 'Быстро создайте помощника с искусственным интеллектом для задач генерации текста с простой настройкой.', workflowUserDescription: 'Визуально создавайте автономные ИИ-процессы простым перетаскиванием.', + dropDSLToCreateApp: 'Перетащите файл DSL сюда, чтобы создать приложение', }, editApp: 'Редактировать информацию', editAppTitle: 'Редактировать информацию о приложении', @@ -140,6 +141,14 @@ const translation = { notConfigured: 'Настройте провайдера, чтобы включить трассировку', moreProvider: 'Больше провайдеров', }, + arize: { + title: 'Arize', + description: 'Корпоративный уровень наблюдаемости LLM, онлайн и оффлайн оценка, мониторинг и эксперименты—на основе OpenTelemetry. Специально разработан для приложений на базе LLM и агентов.', + }, + phoenix: { + title: 'Phoenix', + description: 'Открытая и основанная на OpenTelemetry платформа для наблюдаемости, оценки, инженерии подсказок и экспериментов для ваших рабочих процессов и агентов LLM.', + }, langsmith: { title: 'LangSmith', description: 'Универсальная платформа для разработчиков для каждого этапа жизненного цикла приложения на базе LLM.', @@ -167,6 +176,7 @@ const translation = { description: 'Weave — это открытая платформа для оценки, тестирования и мониторинга приложений LLM.', title: 'Ткать', }, + aliyun: {}, }, answerIcon: { title: 'Использование значка web app для замены 🤖', diff --git a/web/i18n/ru-RU/common.ts b/web/i18n/ru-RU/common.ts index 37d207d357..c25de3beef 100644 --- a/web/i18n/ru-RU/common.ts +++ b/web/i18n/ru-RU/common.ts @@ -58,6 +58,8 @@ const translation = { more: 'Больше', downloadFailed: 'Скачивание не удалось. Пожалуйста, попробуйте еще раз позже.', downloadSuccess: 'Загрузка завершена.', + selectAll: 'Выбрать все', + deSelectAll: 'Снять выделение со всех', }, errorMsg: { fieldRequired: '{{field}} обязательно', @@ -471,7 +473,6 @@ const translation = { apiBasedExtension: { title: 'API-расширения обеспечивают централизованное управление API, упрощая настройку для удобного использования в приложениях Dify.', link: 'Узнайте, как разработать собственное API-расширение.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Добавить API Extension', selector: { title: 'API Extension', diff --git a/web/i18n/ru-RU/dataset-creation.ts b/web/i18n/ru-RU/dataset-creation.ts index 7e44306512..49b2f2087c 100644 --- a/web/i18n/ru-RU/dataset-creation.ts +++ b/web/i18n/ru-RU/dataset-creation.ts @@ -63,7 +63,6 @@ const translation = { run: 'Запустить', firecrawlTitle: 'Извлечь веб-контент с помощью 🔥Firecrawl', firecrawlDoc: 'Документация Firecrawl', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', options: 'Опции', crawlSubPage: 'Сканировать подстраницы', limit: 'Лимит', @@ -88,7 +87,6 @@ const translation = { jinaReaderTitle: 'Конвертируйте весь сайт в Markdown', useSitemapTooltip: 'Следуйте карте сайта, чтобы просканировать сайт. Если нет, Jina Reader будет сканировать итеративно в зависимости от релевантности страницы, выдавая меньшее количество страниц, но более высокого качества.', watercrawlTitle: 'Извлечение веб-контента с помощью Watercrawl', - watercrawlDocLink: 'https://docs.dify.ai/ru/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureWatercrawl: 'Настроить Watercrawl', waterCrawlNotConfigured: 'Watercrawl не настроен', configureFirecrawl: 'Настроить Firecrawl', diff --git a/web/i18n/ru-RU/dataset-documents.ts b/web/i18n/ru-RU/dataset-documents.ts index 735266c087..bbc221fba7 100644 --- a/web/i18n/ru-RU/dataset-documents.ts +++ b/web/i18n/ru-RU/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'Удалить', enableWarning: 'Архивный файл не может быть включен', sync: 'Синхронизировать', + resume: 'Продовжити', + pause: 'Пауза', }, index: { enable: 'Включить', @@ -389,6 +391,8 @@ const translation = { characters_one: 'характер', addChildChunk: 'Добавить дочерний чанк', newChildChunk: 'Новый дочерний чанк', + keywordEmpty: 'Ключевое слово не может быть пустым', + keywordDuplicate: 'Ключевое слово уже существует', }, } diff --git a/web/i18n/ru-RU/dataset.ts b/web/i18n/ru-RU/dataset.ts index 33a4cdf851..fd6839cd33 100644 --- a/web/i18n/ru-RU/dataset.ts +++ b/web/i18n/ru-RU/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'Имя метаданных не может быть пустым', invalid: 'Имя метаданных может содержать только строчные буквы, цифры и знаки нижнего подчеркивания и должно начинаться со строчной буквы.', + tooLong: 'Имя метаданных не может превышать {{max}} символов', }, batchEditMetadata: { applyToAllSelectDocumentTip: 'Автоматически создайте все вышеуказанные редактируемые и новые метаданные для всех выбранных документов, иначе редактирование метаданных будет применяться только к документам с ними.', diff --git a/web/i18n/ru-RU/plugin.ts b/web/i18n/ru-RU/plugin.ts index 3dc48a0327..0bb6c8232e 100644 --- a/web/i18n/ru-RU/plugin.ts +++ b/web/i18n/ru-RU/plugin.ts @@ -137,6 +137,7 @@ const translation = { uploadingPackage: 'Загрузка {{packageName}}...', pluginLoadError: 'Ошибка загрузки плагина', readyToInstallPackage: 'О программе установки следующего плагина', + installWarning: 'Этот плагин не разрешено устанавливать.', }, installFromGitHub: { gitHubRepo: 'Репозиторий GitHub', @@ -199,7 +200,6 @@ const translation = { searchTools: 'Инструменты поиска...', allCategories: 'Все категории', endpointsEnabled: '{{num}} наборы включенных конечных точек', - submitPlugin: 'Отправить плагин', installAction: 'Устанавливать', from: 'От', installFrom: 'УСТАНОВИТЬ С', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Текущая версия Dify не совместима с этим плагином, пожалуйста, обновите до минимально необходимой версии: {{minimalDifyVersion}}', requestAPlugin: 'Запросите плагин', + publishPlugins: 'Публикация плагинов', } export default translation diff --git a/web/i18n/ru-RU/tools.ts b/web/i18n/ru-RU/tools.ts index e1975ee538..caa1959318 100644 --- a/web/i18n/ru-RU/tools.ts +++ b/web/i18n/ru-RU/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'добавить', added: 'добавлено', manageInTools: 'Управлять в инструментах', - emptyTitle: 'Нет доступных инструментов рабочего процесса', - emptyTip: 'Перейдите в "Рабочий процесс -> Опубликовать как инструмент"', - emptyTitleCustom: 'Нет пользовательского инструмента', - emptyTipCustom: 'Создание пользовательского инструмента', + custom: { + title: 'Нет доступного пользовательского инструмента', + tip: 'Создать пользовательский инструмент', + }, + workflow: { + title: 'Нет доступного инструмента рабочего процесса', + tip: 'Публиковать рабочие процессы как инструменты в Студии', + }, + mcp: { + title: 'Нет доступного инструмента MCP', + tip: 'Добавить сервер MCP', + }, + agent: { + title: 'Нет доступной стратегии агента', + }, }, createTool: { title: 'Создать пользовательский инструмент', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Название вызова инструмента для рассуждений агента и подсказок', copyToolName: 'Копировать имя', noTools: 'Инструменты не найдены', + mcp: { + create: { + cardTitle: 'Добавить MCP сервер (HTTP)', + cardLink: 'Узнайте больше об интеграции MCP сервера', + }, + noConfigured: 'Неконфигурированный сервер', + updateTime: 'Обновлено', + toolsCount: '{count} инструментов', + noTools: 'Нет доступных инструментов', + modal: { + title: 'Добавить MCP сервер (HTTP)', + editTitle: 'Редактировать MCP сервер (HTTP)', + name: 'Имя и иконка', + namePlaceholder: 'Назовите ваш MCP сервер', + serverUrl: 'URL сервера', + serverUrlPlaceholder: 'URL конечной точки сервера', + serverUrlWarning: 'Обновление адреса сервера может нарушить работу приложений, которые зависят от этого сервера', + serverIdentifier: 'Идентификатор сервера', + serverIdentifierTip: 'Уникальный идентификатор MCP сервера в рабочем пространстве. Только строчные буквы, цифры, подчеркивания и дефисы. Максимум 24 символа.', + serverIdentifierPlaceholder: 'Уникальный идентификатор, например, мой-сервер-mcp', + serverIdentifierWarning: 'Сервер не будет распознан существующими приложениями после изменения ID', + cancel: 'Отмена', + save: 'Сохранить', + confirm: 'Добавить и авторизовать', + }, + delete: 'Удалить MCP сервер', + deleteConfirmTitle: 'Вы действительно хотите удалить {mcp}?', + operation: { + edit: 'Редактировать', + remove: 'Удалить', + }, + authorize: 'Авторизовать', + authorizing: 'Авторизация...', + authorizingRequired: 'Требуется авторизация', + authorizeTip: 'После авторизации инструменты будут отображены здесь.', + update: 'Обновить', + updating: 'Обновление', + gettingTools: 'Получение инструментов...', + updateTools: 'Обновление инструментов...', + toolsEmpty: 'Инструменты не загружены', + getTools: 'Получить инструменты', + toolUpdateConfirmTitle: 'Обновить список инструментов', + toolUpdateConfirmContent: 'Обновление списка инструментов может повлиять на существующие приложения. Вы хотите продолжить?', + toolsNum: '{count} инструментов включено', + onlyTool: '1 инструмент включен', + identifier: 'Идентификатор сервера (Нажмите, чтобы скопировать)', + server: { + title: 'MCP Сервер', + url: 'URL сервера', + reGen: 'Хотите регенерировать URL сервера?', + addDescription: 'Добавить описание', + edit: 'Редактировать описание', + modal: { + addTitle: 'Добавить описание, чтобы включить MCP сервер', + editTitle: 'Редактировать описание', + description: 'Описание', + descriptionPlaceholder: 'Объясните, что делает этот инструмент и как его должен использовать LLM', + parameters: 'Параметры', + parametersTip: 'Добавьте описания для каждого параметра, чтобы помочь LLM понять их назначение и ограничения.', + parametersPlaceholder: 'Назначение и ограничения параметра', + confirm: 'Активировать MCP сервер', + }, + publishTip: 'Приложение не опубликовано. Пожалуйста, сначала опубликуйте приложение.', + }, + }, } export default translation diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 7b22876f5a..aecd9e652c 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Установить значение переменной', needConnectTip: 'Этот шаг ни к чему не подключен', maxTreeDepth: 'Максимальный предел {{depth}} узлов на ветку', - needEndNode: 'Необходимо добавить блок "Конец"', - needAnswerNode: 'Необходимо добавить блок "Ответ"', workflowProcess: 'Процесс рабочего процесса', notRunning: 'Еще не запущено', previewPlaceholder: 'Введите текст в поле ниже, чтобы начать отладку чат-бота', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Узнать больше', copy: 'Копировать', duplicate: 'Дублировать', - addBlock: 'Добавить блок', pasteHere: 'Вставить сюда', pointerMode: 'Режим указателя', handMode: 'Режим руки', @@ -115,6 +112,9 @@ const translation = { exitVersions: 'Выходные версии', exportSVG: 'Экспортировать как SVG', publishUpdate: 'Опубликовать обновление', + addBlock: 'Добавить узел', + needAnswerNode: 'В узел ответа необходимо добавить', + needEndNode: 'Узел конца должен быть добавлен', }, env: { envPanelTitle: 'Переменные среды', @@ -129,6 +129,8 @@ const translation = { value: 'Значение', valuePlaceholder: 'Значение переменной среды', secretTip: 'Используется для определения конфиденциальной информации или данных, с настройками DSL, настроенными для предотвращения утечки.', + description: 'Описание', + descriptionPlaceholder: 'Опишите переменную', }, export: { title: 'Экспортировать секретные переменные среды?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} шагов вперед', sessionStart: 'Начало сеанса', currentState: 'Текущее состояние', - nodeTitleChange: 'Изменено название блока', - nodeDescriptionChange: 'Изменено описание блока', - nodeDragStop: 'Блок перемещен', - nodeChange: 'Блок изменен', - nodeConnect: 'Блок подключен', - nodePaste: 'Блок вставлен', - nodeDelete: 'Блок удален', - nodeAdd: 'Блок добавлен', - nodeResize: 'Размер блока изменен', noteAdd: 'Заметка добавлена', noteChange: 'Заметка изменена', noteDelete: 'Заметка удалена', - edgeDelete: 'Блок отключен', + nodeDragStop: 'Узел перемещен', + nodeResize: 'Узел изменен в размере', + nodeTitleChange: 'Название узла изменено', + edgeDelete: 'Узел отключен', + nodeConnect: 'Узел подключен', + nodeDelete: 'Узел удален', + nodePaste: 'Узел вставлен', + nodeChange: 'Узел изменен', + nodeAdd: 'Узел добавлен', + nodeDescriptionChange: 'Описание узла изменено', }, errorMsg: { fieldRequired: '{{field}} обязательно для заполнения', @@ -217,8 +219,6 @@ const translation = { loop: 'Цикл', }, tabs: { - 'searchBlock': 'Поиск блока', - 'blocks': 'Блоки', 'searchTool': 'Поиск инструмента', 'tools': 'Инструменты', 'allTool': 'Все', @@ -232,6 +232,8 @@ const translation = { 'noResult': 'Ничего не найдено', 'plugin': 'Плагин', 'agent': 'Агентская стратегия', + 'blocks': 'Узлы', + 'searchBlock': 'Поиск узла', }, blocks: { 'start': 'Начало', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Поле ввода пользователя', - changeBlock: 'Изменить блок', helpLink: 'Ссылка на справку', about: 'О программе', createdBy: 'Создано ', nextStep: 'Следующий шаг', - addNextStep: 'Добавить следующий блок в этот рабочий процесс', - selectNextStep: 'Выбрать следующий блок', runThisStep: 'Выполнить этот шаг', checklist: 'Контрольный список', checklistTip: 'Убедитесь, что все проблемы решены перед публикацией', checklistResolved: 'Все проблемы решены', - organizeBlocks: 'Организовать блоки', change: 'Изменить', optional: '(необязательно)', moveToThisNode: 'Перейдите к этому узлу', + selectNextStep: 'Выберите следующий шаг', + organizeBlocks: 'Организовать узлы', + addNextStep: 'Добавьте следующий шаг в этот рабочий процесс', + changeBlock: 'Изменить узел', + minimize: 'Выйти из полноэкранного режима', + maximize: 'Максимизировать холст', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retryFailedTimes: 'Повторные попытки {{times}} не увенчались успехом', retries: '{{число}} Повторных попыток', }, + typeSwitch: {}, }, start: { required: 'обязательно', @@ -535,6 +540,10 @@ const translation = { placeholder: 'Вставьте сюда строку cURL', title: 'Импорт из cURL', }, + verifySSL: { + title: 'Проверить SSL-сертификат', + warningTooltip: 'Отключение проверки SSL не рекомендуется для производственных сред. Это следует использовать только в разработке или тестировании, так как это делает соединение уязвимым для угроз безопасности, таких как атаки «человек посередине».', + }, }, code: { inputVars: 'Входные переменные', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Входные переменные', outputVars: { className: 'Имя класса', + usage: 'Информация об использовании модели', }, class: 'Класс', classNamePlaceholder: 'Введите имя вашего класса', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Входная переменная', + outputVars: { + isSuccess: 'Успешно. В случае успеха значение равно 1, в случае сбоя - 0.', + errorReason: 'Причина ошибки', + usage: 'Информация об использовании модели', + }, extractParameters: 'Извлечь параметры', importFromTool: 'Импортировать из инструментов', addExtractParameter: 'Добавить параметр для извлечения', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Расширенные настройки', reasoningMode: 'Режим рассуждения', reasoningModeTip: 'Вы можете выбрать соответствующий режим рассуждения, основываясь на способности модели реагировать на инструкции для вызова функций или подсказки.', - isSuccess: 'Успешно. В случае успеха значение равно 1, в случае сбоя - 0.', - errorReason: 'Причина ошибки', }, iteration: { deleteTitle: 'Удалить узел итерации?', @@ -917,6 +930,35 @@ const translation = { releaseNotesPlaceholder: 'Опишите, что изменилось', defaultName: 'Без названия версия', }, + debug: { + noData: { + description: 'Результаты последнего запуска будут отображены здесь', + runThisNode: 'Запустите этот узел', + }, + variableInspect: { + trigger: { + stop: 'Стоп, беги', + cached: 'Посмотреть кэшированные переменные', + normal: 'Переменная Инспекция', + clear: 'Ясно', + running: 'Кэширование текущего состояния', + }, + clearAll: 'Сбросить все', + edited: 'Отредактировано', + emptyLink: 'Узнать больше', + systemNode: 'Система', + chatNode: 'Разговор', + clearNode: 'Очистить кэшированную переменную', + reset: 'Сбросить до последнего значения выполнения', + view: 'Просмотр журнала', + title: 'Переменная Инспекция', + resetConversationVar: 'Сбросить переменную разговора до значения по умолчанию', + envNode: 'Окружающая среда', + emptyTip: 'После прохождения через узел на холсте или выполнения узла шаг за шагом вы можете просмотреть текущее значение переменной узла в инспекторе переменных.', + }, + lastRunTab: 'Последний запуск', + settingsTab: 'Настройки', + }, } export default translation diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index 4ac445872d..152b7d63dc 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -97,6 +97,7 @@ const translation = { chatbotShortDescription: 'Chatbot, ki temelji na LLM, s preprosto nastavitvijo', chooseAppType: 'Izberite vrsto aplikacije', learnMore: 'Izvedi več', + dropDSLToCreateApp: 'Spustite DSL datoteko sem, da ustvarite aplikacijo', }, editApp: 'Uredi informacije', editAppTitle: 'Uredi informacije o aplikaciji', @@ -145,6 +146,14 @@ const translation = { notConfigured: 'Konfigurirajte ponudnika za omogočanje sledenja', moreProvider: 'Več ponudnikov', }, + arize: { + title: 'Arize', + description: 'Podjetniško opazovanje LLM, spletno in nespletno vrednotenje, nadzorovanje in eksperimentiranje — s podporo OpenTelemetry. Namenjeno aplikacijam, ki temeljijo na LLM in agentih.', + }, + phoenix: { + title: 'Phoenix', + description: 'Odprtokodna in na OpenTelemetry osnovana platforma za opazovanje, vrednotenje, inženiring pozivov ter eksperimentiranje za vaše LLM poteke dela in agente.', + }, langsmith: { title: 'LangSmith', description: 'Vse-v-enem razvijalska platforma za vsak korak življenjskega cikla aplikacije, ki jo poganja LLM.', @@ -172,6 +181,7 @@ const translation = { title: 'Tkanje', description: 'Weave je odprtokodna platforma za vrednotenje, testiranje in spremljanje aplikacij LLM.', }, + aliyun: {}, }, mermaid: { handDrawn: 'Ročno narisano', diff --git a/web/i18n/sl-SI/common.ts b/web/i18n/sl-SI/common.ts index b43a2dbbb2..2da1acdbf9 100644 --- a/web/i18n/sl-SI/common.ts +++ b/web/i18n/sl-SI/common.ts @@ -58,6 +58,8 @@ const translation = { more: 'Več', downloadSuccess: 'Prenos končan.', format: 'Format', + selectAll: 'Izberi vse', + deSelectAll: 'Odberi vse', }, errorMsg: { fieldRequired: '{{field}} je obvezno', @@ -464,7 +466,6 @@ const translation = { apiBasedExtension: { title: 'Razširitve API omogočajo centralizirano upravljanje API, kar poenostavi konfiguracijo za enostavno uporabo v aplikacijah Dify.', link: 'Naučite se, kako razviti svojo API razširitev.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Dodaj API razširitev', selector: { title: 'API razširitev', @@ -693,7 +694,6 @@ const translation = { type: 'Vrsta', link: 'Preberite, kako razvijete lastno razširitev API-ja.', title: 'Razširitve API zagotavljajo centralizirano upravljanje API, kar poenostavlja konfiguracijo za enostavno uporabo v aplikacijah Dify.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Dodajanje razširitve API-ja', }, about: { diff --git a/web/i18n/sl-SI/dataset-creation.ts b/web/i18n/sl-SI/dataset-creation.ts index 72e71049ac..bbe98799d2 100644 --- a/web/i18n/sl-SI/dataset-creation.ts +++ b/web/i18n/sl-SI/dataset-creation.ts @@ -71,7 +71,6 @@ const translation = { run: 'Zaženi', firecrawlTitle: 'Izvleci spletno vsebino z 🔥Firecrawl', firecrawlDoc: 'Firecrawl dokumentacija', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', jinaReaderTitle: 'Pretvori celotno stran v Markdown', jinaReaderDoc: 'Več o Jina Reader', jinaReaderDocLink: 'https://jina.ai/reader', @@ -97,7 +96,6 @@ const translation = { waterCrawlNotConfigured: 'Watercrawl ni konfiguriran', watercrawlDoc: 'Watercrawl dokumentacija', configureJinaReader: 'Konfigurirajte Jina Reader', - watercrawlDocLink: 'https://docs.dify.ai/sl/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureFirecrawl: 'Konfigurirajte Firecrawl', watercrawlTitle: 'Izvleci vsebino z interneta z Watercrawl', }, diff --git a/web/i18n/sl-SI/dataset-documents.ts b/web/i18n/sl-SI/dataset-documents.ts index 78d63c9e29..e76055ad79 100644 --- a/web/i18n/sl-SI/dataset-documents.ts +++ b/web/i18n/sl-SI/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'Izbriši', enableWarning: 'Arhivirane datoteke ni mogoče omogočiti', sync: 'Sinhroniziraj', + pause: 'Zaustavi', + resume: 'Nadaljuj', }, index: { enable: 'Omogoči', @@ -332,7 +334,7 @@ const translation = { previewTip: 'Predogled odstavkov bo na voljo po zaključku vdelave', hierarchical: 'Starš-otrok', childMaxTokens: 'Otrok', - pause: 'Pavza', + pause: 'Zaustavi', parentMaxTokens: 'Starš', }, segment: { @@ -389,6 +391,8 @@ const translation = { chunk: 'Kos', addChunk: 'Dodajanje kosa', childChunkAdded: 'Dodan je 1 kos otroka', + keywordDuplicate: 'Ključna beseda že obstaja', + keywordEmpty: 'Ključna beseda ne more biti prazna', }, } diff --git a/web/i18n/sl-SI/dataset.ts b/web/i18n/sl-SI/dataset.ts index a9f9ccbad7..20403bd722 100644 --- a/web/i18n/sl-SI/dataset.ts +++ b/web/i18n/sl-SI/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'Ime metapodatkov ne more biti prazno', invalid: 'Ime metapodatkov lahko vsebuje samo male črke, številke in podčrtaje ter se mora začeti z malo črko.', + tooLong: 'Ime metapodatkov ne sme presegati {{max}} znakov', }, batchEditMetadata: { editMetadata: 'Uredi metapodatke', diff --git a/web/i18n/sl-SI/plugin.ts b/web/i18n/sl-SI/plugin.ts index 5580d4b1f4..e3be40c4d5 100644 --- a/web/i18n/sl-SI/plugin.ts +++ b/web/i18n/sl-SI/plugin.ts @@ -140,6 +140,7 @@ const translation = { install: 'Namestite', pluginLoadError: 'Napaka pri nalaganju vtičnika', installPlugin: 'Namestite vtičnik', + installWarning: 'Ta vtičnik ni dovoljen za namestitev.', }, installFromGitHub: { updatePlugin: 'Posodobite vtičnik iz GitHuba', @@ -209,9 +210,9 @@ const translation = { findMoreInMarketplace: 'Poiščite več v Tržnici', install: '{{num}} namestitev', allCategories: 'Vse kategorije', - submitPlugin: 'Oddajte vtičnik', difyVersionNotCompatible: 'Trenutna različica Dify ni združljiva s to vtičnico, prosimo, posodobite na minimalno zahtevano različico: {{minimalDifyVersion}}', requestAPlugin: 'Zahtevajte vtičnik', + publishPlugins: 'Objavljanje vtičnikov', } export default translation diff --git a/web/i18n/sl-SI/tools.ts b/web/i18n/sl-SI/tools.ts index e557725462..d83f218f68 100644 --- a/web/i18n/sl-SI/tools.ts +++ b/web/i18n/sl-SI/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'dodaj', added: 'dodano', manageInTools: 'Upravljaj v Orodjih', - emptyTitle: 'Orodje za potek dela ni na voljo', - emptyTip: 'Pojdite na "Potek dela -> Objavi kot orodje"', - emptyTipCustom: 'Ustvarjanje orodja po meri', - emptyTitleCustom: 'Orodje po meri ni na voljo', + custom: { + title: 'Žiadne prispôsobené nástroje nie sú k dispozícii', + tip: 'Vytvorte prispôsobený nástroj', + }, + workflow: { + title: 'Žiadny nástroj pracovného postupu nie je k dispozícii', + tip: 'Publikujte pracovné postupy ako nástroje v Studio', + }, + mcp: { + title: 'Žiadny nástroj MCP nie je k dispozícii', + tip: 'Pridajte server MCP', + }, + agent: { + title: 'Žiadna stratégia agenta nie je k dispozícii', + }, }, createTool: { title: 'Ustvari prilagojeno orodje', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Ime klica orodja za sklepanja in pozivanje agenta', copyToolName: 'Kopiraj ime', noTools: 'Orodja niso bila najdena', + mcp: { + create: { + cardTitle: 'Dodaj strežnik MCP (HTTP)', + cardLink: 'Več o integraciji strežnika MCP', + }, + noConfigured: 'Nekonfiguriran strežnik', + updateTime: 'Posodobljeno', + toolsCount: '{count} orodij', + noTools: 'Na voljo ni orodij', + modal: { + title: 'Dodaj strežnik MCP (HTTP)', + editTitle: 'Uredi strežnik MCP (HTTP)', + name: 'Ime in ikona', + namePlaceholder: 'Poimenuj svoj strežnik MCP', + serverUrl: 'URL strežnika', + serverUrlPlaceholder: 'URL do končne točke strežnika', + serverUrlWarning: 'Posodobitev naslova strežnika lahko prekine aplikacije, ki so odvisne od tega strežnika', + serverIdentifier: 'Identifikator strežnika', + serverIdentifierTip: 'Edinstven identifikator za strežnik MCP v delovnem prostoru. Samo male črke, številke, podčrtaji in vezaji. Največ 24 znakov.', + serverIdentifierPlaceholder: 'Edinstven identifikator, npr. moj-mcp-streznik', + serverIdentifierWarning: 'Strežnik po spremembi ID-ja ne bo prepoznan s strani obstoječih aplikacij', + cancel: 'Prekliči', + save: 'Shrani', + confirm: 'Dodaj in avtoriziraj', + }, + delete: 'Odstrani strežnik MCP', + deleteConfirmTitle: 'Odstraniti {mcp}?', + operation: { + edit: 'Uredi', + remove: 'Odstrani', + }, + authorize: 'Avtoriziraj', + authorizing: 'Avtoriziranje...', + authorizingRequired: 'Avtorizacija je zahtevana', + authorizeTip: 'Po avtorizaciji bodo orodja prikazana tukaj.', + update: 'Posodobi', + updating: 'Posodabljanje...', + gettingTools: 'Pridobivanje orodij...', + updateTools: 'Posodabljanje orodij...', + toolsEmpty: 'Orodja niso naložena', + getTools: 'Pridobi orodja', + toolUpdateConfirmTitle: 'Posodobi seznam orodij', + toolUpdateConfirmContent: 'Posodobitev seznama orodij lahko vpliva na obstoječe aplikacije. Želite nadaljevati?', + toolsNum: 'Vključenih {count} orodij', + onlyTool: 'Vključeno 1 orodje', + identifier: 'Identifikator strežnika (Kliknite za kopiranje)', + server: { + title: 'Strežnik MCP', + url: 'URL strežnika', + reGen: 'Želite ponovno ustvariti URL strežnika?', + addDescription: 'Dodaj opis', + edit: 'Uredi opis', + modal: { + addTitle: 'Dodajte opis za omogočitev strežnika MCP', + editTitle: 'Uredi opis', + description: 'Opis', + descriptionPlaceholder: 'Pojasnite, kaj to orodje počne in kako naj ga uporablja LLM', + parameters: 'Parametri', + parametersTip: 'Dodajte opise za vsak parameter, da pomagate LLM razumeti njihov namen in omejitve.', + parametersPlaceholder: 'Namen in omejitve parametra', + confirm: 'Omogoči strežnik MCP', + }, + publishTip: 'Aplikacija ni objavljena. Najprej objavite aplikacijo.', + }, + }, } export default translation diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index a0f4f8d95e..a7c2626264 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -1,901 +1,470 @@ const translation = { common: { - undo: 'Razveljavi', - redo: 'Uveljavi', - editing: 'Urejanje', - autoSaved: 'Samodejno shranjeno', - unpublished: 'Nepublicirano', - published: 'Objavljeno', - publish: 'Objavi', - update: 'Posodobi', - run: 'Zaženi', - running: 'V teku', - inRunMode: 'V načinu zagona', - inPreview: 'V predogledu', - inPreviewMode: 'V načinu predogleda', - preview: 'Predogled', - viewRunHistory: 'Ogled zgodovine zagona', - runHistory: 'Zgodovina zagona', - goBackToEdit: 'Nazaj na urejevalnik', - conversationLog: 'Zapisnik pogovora', - features: 'Značilnosti', - debugAndPreview: 'Predogled', - restart: 'Ponovni zagon', - currentDraft: 'Trenutni osnutek', - currentDraftUnpublished: 'Trenutni osnutek ni objavljen', - latestPublished: 'Zadnje objavljeno', - publishedAt: 'Objavljeno ob', - restore: 'Obnovi', - runApp: 'Zaženi aplikacijo', - batchRunApp: 'Serijski zagon aplikacije', - accessAPIReference: 'Dostop do API referenc', - embedIntoSite: 'Vdelaj v spletno stran', - addTitle: 'Dodaj naslov...', - addDescription: 'Dodaj opis...', - noVar: 'Ni spremenljivke', - searchVar: 'Išči spremenljivko', - variableNamePlaceholder: 'Ime spremenljivke', - setVarValuePlaceholder: 'Nastavi vrednost spremenljivke', - needConnectTip: 'Ta korak ni povezan z ničemer', - maxTreeDepth: 'Največja omejitev je {{depth}} vozlišč na vejo', - needEndNode: 'Dodati je treba zaključni blok', - needAnswerNode: 'Dodati je treba blok z odgovorom', - workflowProcess: 'Proces delovnega toka', - notRunning: 'Še ni v teku', - previewPlaceholder: 'Vnesite vsebino v spodnje polje za začetek odpravljanja napak klepetalnika', effectVarConfirm: { title: 'Odstrani spremenljivko', - content: 'Spremenljivka se uporablja v drugih vozliščih. Ali jo kljub temu želite odstraniti?', + content: 'Spremenljivka se uporablja v drugih vozliščih. Ali jo še vedno želite odstraniti?', }, - insertVarTip: 'Pritisnite tipko \'/\' za hitro vstavljanje', - processData: 'Obdelava podatkov', - input: 'Vnos', - output: 'Izhod', - jinjaEditorPlaceholder: 'Vnesite \'/\' ali \'{\' za vstavljanje spremenljivke', - viewOnly: 'Samo ogled', - showRunHistory: 'Prikaži zgodovino zagona', - enableJinja: 'Omogoči podporo za Jinja predloge', - learnMore: 'Več informacij', - copy: 'Kopiraj', - duplicate: 'Podvoji', - addBlock: 'Dodaj blok', - pasteHere: 'Prilepi tukaj', - pointerMode: 'Način kazalca', - handMode: 'Način roke', - model: 'Model', - workflowAsTool: 'Potek dela kot orodje', - configureRequired: 'Potrebna konfiguracija', - configure: 'Konfiguriraj', - manageInTools: 'Upravljaj v Orodjih', - workflowAsToolTip: 'Po posodobitvi poteka dela je potrebno ponovno konfigurirati orodje.', - viewDetailInTracingPanel: 'Oglejte si podrobnosti', - syncingData: 'Sinhronizacija podatkov, le nekaj sekund.', - importDSL: 'Uvozi DSL', - importDSLTip: 'Trenutni osnutek bo prepisan. Pred uvozom izvozite delovni tok kot varnostno kopijo.', - backupCurrentDraft: 'Varnostno kopiraj trenutni osnutek', - chooseDSL: 'Izberite DSL(yml) datoteko', - overwriteAndImport: 'Prepiši in uvozi', - importFailure: 'Uvoz ni uspel', - importSuccess: 'Uvoz uspešen', - parallelRun: 'Vzporedni zagon', parallelTip: { click: { + desc: 'dodati', title: 'Klikni', - desc: ' za dodajanje', }, drag: { + desc: 'povezati', title: 'Povleci', - desc: ' za povezavo', }, - limit: 'Vzporednost je omejena na {{num}} vej.', - depthLimit: 'Omejitev gnezdenja vzporednih slojev na {{num}} slojev', + depthLimit: 'Meja paralelnega gnezdenja plasti {{num}} plasti', + limit: 'Paralelizem je omejen na {{num}} veje.', }, - disconnect: 'Prekini povezavo', - jumpToNode: 'Skoči na to vozlišče', - addParallelNode: 'Dodaj vzporedno vozlišče', - parallel: 'VZPOREDNO', - branch: 'VEJA', - fileUploadTip: 'Funkcije nalaganja slik so nadgrajene na nalaganje datotek.', - featuresDocLink: 'Izvedi več', - featuresDescription: 'Izboljšajte uporabniško izkušnjo spletne aplikacije', - ImageUploadLegacyTip: 'Zdaj lahko ustvarite spremenljivke vrste datoteke v začetnem obrazcu. V prihodnje ne bomo več podpirali funkcije nalaganja slik.', - importWarning: 'Previdnost', - importWarningDetails: 'Razlika v različici DSL lahko vpliva na nekatere funkcije', - openInExplore: 'Odpri v razišči', - addFailureBranch: 'Dodajanje veje »Fail«', - onFailure: 'O neuspehu', - noHistory: 'Brez zgodovine', - loadMore: 'Nalaganje več potekov dela', - exportJPEG: 'Izvozi kot JPEG', - exportPNG: 'Izvozi kot PNG', - noExist: 'Nobena takšna spremenljivka ne obstaja.', - exitVersions: 'Izhodne različice', versionHistory: 'Zgodovina različic', + published: 'Objavljeno', + run: 'Teči', + featuresDocLink: 'Nauči se več', + notRunning: 'Še ne teče', + exportImage: 'Izvozi sliko', + openInExplore: 'Odpri v Raziskovanju', publishUpdate: 'Objavi posodobitev', + disconnect: 'Odklop', + exportJPEG: 'Izvozi kot JPEG', exportSVG: 'Izvozi kot SVG', - referenceVar: 'Referenčna spremenljivka', - exportImage: 'Izvozi sliko', + model: 'Model', + restart: 'Znova zaženi', + running: 'Tek', + undo: 'Razveljavi', + enableJinja: 'Omogoči podporo za Jinja predloge', + publish: 'Objavi', + importSuccess: 'Uvoz uspešen', + workflowAsTool: 'Delovni potek kot orodje', + update: 'Posodobitev', + jumpToNode: 'Preskoči na ta vozel', + publishedAt: 'Objavljeno', + addParallelNode: 'Dodaj paralelni vozlišče', + inPreview: 'V predogledu', + workflowAsToolTip: 'Zaradi posodobitve delovnega poteka je potrebna ponovna konfiguracija orodja.', + variableNamePlaceholder: 'Ime spremenljivke', + needEndNode: 'Skrivnostna vozlišča je treba dodati.', + onFailure: 'O neuspehu', + embedIntoSite: 'Vstavite v spletno stran', + conversationLog: 'Pogovor Log', + accessAPIReference: 'Dostop do referenčnega API-ja', + inPreviewMode: 'V načinu predogleda', + previewPlaceholder: 'Vnesite vsebino v spodnje polje, da začnete odpravljati napake v chatbotu', + input: 'Vnos', + importDSLTip: 'Trenutni osnutek bo prepisan. Izvozite delovni postopek kot varnostno kopijo pred uvozom.', + duplicate: 'Podvojiti', + loadMore: 'Naloži več', + addTitle: 'Dodajte naslov...', + goBackToEdit: 'Pojdi nazaj k uredniku', + needAnswerNode: 'Skrivnostni element mora biti dodan.', + needConnectTip: 'Ta korak ni povezan z ničemer.', + searchVar: 'Iskalna spremenljivka', + branch: 'VEJA', + viewRunHistory: 'Poglej zgodovino izvajanja', + learnMore: 'Nauči se več', + workflowProcess: 'Delovni postopek', + preview: 'Predogled', + output: 'Izhod', + viewDetailInTracingPanel: 'Oglejte si podrobnosti', + debugAndPreview: 'Predogled', + restore: 'Obnovi', + latestPublished: 'Najnovejša objavljena', + importDSL: 'Uvozi DSL', + viewOnly: 'Samo za ogled', + insertVarTip: 'Pritisnite tipko \'/\' za hitro vstavljanje', + currentDraftUnpublished: 'Trenutna osnutek neobjavljeno', + showRunHistory: 'Prikaži zgodovino izvajanja', + runHistory: 'Zgodovina izvajanja', + fileUploadTip: 'Funkcije nalaganja slik so bile nadgrajene na nalaganje datotek.', + backupCurrentDraft: 'Varnostno kopiraj trenutni osnutek', + overwriteAndImport: 'Prepiši in uvozi', + features: 'Značilnosti', + exportPNG: 'Izvozi kot PNG', + parallelRun: 'Paralelni tek', + chooseDSL: 'Izberi DSL datoteko', + unpublished: 'Nepublikirano', + pasteHere: 'Prilepite tukaj', + featuresDescription: 'Izboljšanje uporabniške izkušnje spletne aplikacije', + exitVersions: 'Izhodne različice', + editing: 'Urejanje', + addFailureBranch: 'Dodaj neuspešno vejo', + syncingData: 'Sinhronizacija podatkov, samo nekaj sekund.', + noVar: 'Brez spremenljivke', + runApp: 'Zaženi aplikacijo', + ImageUploadLegacyTip: 'Zdaj lahko v začetni obliki ustvarite spremenljivke datotečnega tipa. V prihodnje ne bomo več podpirali funkcije nalaganja slik.', + importWarning: 'Opozorilo', + copy: 'Kopirati', + redo: 'Ponovno naredi', + currentDraft: 'Trenutni osnutek', + manageInTools: 'Upravljajte v orodjih', + parallel: 'PARALELNO', + importWarningDetails: 'Razlika v različici DSL lahko vpliva na nekatere funkcije', + addDescription: 'Dodajte opis...', + maxTreeDepth: 'Največje število {{depth}} vozlišč na vejo', + jinjaEditorPlaceholder: 'Vnesite \'/\' ali \'{\', da vstavite spremenljivko', + batchRunApp: 'Program za serijsko izvajanje', + importFailure: 'Uvoz ni uspel', + handMode: 'Ročni način', + processData: 'Obdelava podatkov', + addBlock: 'Dodaj vozlišče', + noHistory: 'Brez zgodovine', + configureRequired: 'Konfigurirajte zahteve', + setVarValuePlaceholder: 'Nastavi spremenljivko', + pointerMode: 'Način s kazalcem', + autoSaved: 'Samodejno shranjeno', + configure: 'Konfiguriraj', + inRunMode: 'V načinu izvajanja', }, env: { - envPanelTitle: 'Spremenljivke okolja', - envDescription: 'Spremenljivke okolja se uporabljajo za shranjevanje zasebnih informacij in poverilnic. So samo za branje in jih je mogoče ločiti od DSL datoteke med izvozom.', - envPanelButton: 'Dodaj spremenljivko', modal: { - title: 'Dodaj spremenljivko okolja', - editTitle: 'Uredi spremenljivko okolja', - type: 'Vrsta', - name: 'Ime', - namePlaceholder: 'ime okolja', value: 'Vrednost', + title: 'Dodaj okoljsko spremenljivko', + name: 'Ime', valuePlaceholder: 'vrednost okolja', - secretTip: 'Uporablja se za definiranje občutljivih informacij ali podatkov, s konfiguriranimi nastavitvami DSL za preprečevanje uhajanja.', + namePlaceholder: 'ime okolja', + type: 'Tip', + editTitle: 'Uredi okoljsko spremenljivko', + secretTip: 'Uporablja se za opredelitev občutljivih informacij ali podatkov, s konfiguriranimi nastavitvami DSL za preprečevanje puščanja.', + description: 'Opis', + descriptionPlaceholder: 'Opisujte spremenljivko', }, export: { - title: 'Izvoziti skrivne spremenljivke okolja?', - checkbox: 'Izvozi skrivne vrednosti', - ignore: 'Izvozi DSL', export: 'Izvozi DSL z skrivnimi vrednostmi', + ignore: 'Izvoz DSL', + checkbox: 'Izvozi tajne vrednosti', + title: 'Izvozi skrivne okoljske spremenljivke?', }, - chatVariable: { - panelTitle: 'Spremenljivke pogovora', - panelDescription: 'Spremenljivke pogovora se uporabljajo za shranjevanje interaktivnih informacij, ki jih mora LLM zapomniti, vključno z zgodovino pogovorov, naloženimi datotekami, uporabniškimi nastavitvami. So za branje in pisanje.', - docLink: 'Obiščite naše dokumente za več informacij.', - button: 'Dodaj spremenljivko', - modal: { - title: 'Dodaj spremenljivko pogovora', - editTitle: 'Uredi spremenljivko pogovora', - name: 'Ime', - namePlaceholder: 'Ime spremenljivke', - type: 'Vrsta', - value: 'Privzeta vrednost', - valuePlaceholder: 'Privzeta vrednost, pustite prazno, če je ne želite nastaviti', - description: 'Opis', - descriptionPlaceholder: 'Opišite spremenljivko', - editInJSON: 'Uredi v JSON', - oneByOne: 'Dodaj eno po eno', - editInForm: 'Uredi v obrazcu', - arrayValue: 'Vrednost', - addArrayValue: 'Dodaj vrednost', - objectKey: 'Ključ', - objectType: 'Vrsta', - objectValue: 'Privzeta vrednost', - }, - storedContent: 'Shranjena vsebina', - updatedAt: 'Posodobljeno ob', - }, - changeHistory: { - title: 'Zgodovina sprememb', - placeholder: 'Še niste ničesar spremenili', - clearHistory: 'Počisti zgodovino', - hint: 'Namig', - hintText: 'Vaša dejanja urejanja se spremljajo v zgodovini sprememb, ki se hrani na vaši napravi med trajanjem te seje. Ta zgodovina se bo izbrisala, ko zapustite urejevalnik.', - stepBackward_one: '{{count}} korak nazaj', - stepBackward_other: '{{count}} korakov nazaj', - stepForward_one: '{{count}} korak naprej', - stepForward_other: '{{count}} korakov naprej', - sessionStart: 'Začetek seje', - currentState: 'Trenutno stanje', - nodeTitleChange: 'Naslov bloka spremenjen', - nodeDescriptionChange: 'Opis bloka spremenjen', - nodeDragStop: 'Blok premaknjen', - nodeChange: 'Blok spremenjen', - nodeConnect: 'Blok povezan', - nodePaste: 'Blok prilepljen', - nodeDelete: 'Blok izbrisan', - nodeAdd: 'Blok dodan', - nodeResize: 'Velikost bloka spremenjena', - noteAdd: 'Opomba dodana', - noteChange: 'Opomba spremenjena', - noteDelete: 'Opomba izbrisana', - edgeDelete: 'Blok prekinjen', - }, - errorMsg: { - fieldRequired: '{{field}} je obvezen', - rerankModelRequired: 'Pred vklopom modela za ponovno razvrščanje, prosimo potrdite, da je bil model uspešno konfiguriran v nastavitvah.', - authRequired: 'Potrebna je avtorizacija', - invalidJson: '{{field}} je neveljaven JSON', - fields: { - variable: 'Ime spremenljivke', - variableValue: 'Vrednost spremenljivke', - code: 'Koda', - model: 'Model', - rerankModel: 'Model za ponovno razvrščanje', - }, - invalidVariable: 'Neveljavna spremenljivka', - }, - singleRun: { - testRun: 'Testni zagon', - startRun: 'Začni zagon', - running: 'V teku', - testRunIteration: 'Iteracija testnega zagona', - back: 'Nazaj', - iteration: 'Iteracija', - }, - tabs: { - 'searchBlock': 'Iskanje bloka', - 'blocks': 'Bloki', - 'searchTool': 'Iskanje orodja', - 'tools': 'Orodja', - 'allTool': 'Vsa', - 'builtInTool': 'Vgrajena', - 'customTool': 'Prilagojena', - 'workflowTool': 'Potek dela', - 'question-understand': 'Razumevanje vprašanja', - 'logic': 'Logika', - 'transform': 'Pretvorba', - 'utilities': 'Pripomočki', - 'noResult': 'Ni najdenih zadetkov', - }, - blocks: { - 'start': 'Začetek', - 'end': 'Konec', - 'answer': 'Odgovor', - 'llm': 'LLM', - 'knowledge-retrieval': 'Pridobivanje znanja', - 'question-classifier': 'Klasifikator vprašanj', - 'if-else': 'IF/ELSE', - 'code': 'Koda', - 'template-transform': 'Predloga', - 'http-request': 'HTTP zahteva', - 'variable-assigner': 'Dodeljevalec spremenljivk', - 'variable-aggregator': 'Zbiralnik spremenljivk', - 'assigner': 'Dodeljevalec spremenljivk', - 'iteration-start': 'Začetek iteracije', - 'iteration': 'Iteracija', - 'parameter-extractor': 'Izvleček parametrov', - }, - blocksAbout: { - 'start': 'Določite začetne parametre za zagon delovnega toka', - 'end': 'Določite konec in vrsto rezultata delovnega toka', - 'answer': 'Določite vsebino odgovora v klepetalni konverzaciji', - 'llm': 'Klicanje velikih jezikovnih modelov za odgovarjanje na vprašanja ali obdelavo naravnega jezika', - 'knowledge-retrieval': 'Omogoča iskanje vsebine, povezane z uporabnikovimi vprašanji, iz baze znanja', - 'question-classifier': 'Določite pogoje za klasifikacijo uporabniških vprašanj; LLM lahko določi, kako se bo konverzacija razvijala glede na klasifikacijski opis', - 'if-else': 'Omogoča razdelitev delovnega toka na dve veji glede na pogoje if/else', - 'code': 'Izvedite del kode Python ali NodeJS za implementacijo prilagojene logike', - 'template-transform': 'Pretvorite podatke v niz z uporabo Jinja predloge', - 'http-request': 'Omogoča pošiljanje strežniških zahtev preko HTTP protokola', - 'variable-assigner': 'Združi spremenljivke več vej v eno spremenljivko za enotno konfiguracijo vozlišč nižje v poteku.', - 'assigner': 'Vozlišče za dodelitev spremenljivk se uporablja za dodelitev vrednosti pisnim spremenljivkam (kot so spremenljivke konverzacije).', - 'variable-aggregator': 'Združi spremenljivke več vej v eno spremenljivko za enotno konfiguracijo vozlišč nižje v poteku.', - 'iteration': 'Izvedite več korakov na seznamu objektov, dokler niso vsi rezultati izpisani.', - 'parameter-extractor': 'Uporabite LLM za izvleček strukturiranih parametrov iz naravnega jezika za klice orodij ali HTTP zahteve.', - }, - operator: { - zoomIn: 'Povečaj', - zoomOut: 'Pomanjšaj', - zoomTo50: 'Povečaj na 50%', - zoomTo100: 'Povečaj na 100%', - zoomToFit: 'Prilagodi velikost', - }, - panel: { - userInputField: 'Vnosno polje uporabnika', - changeBlock: 'Spremeni blok', - helpLink: 'Povezava za pomoč', - about: 'O', - createdBy: 'Ustvaril ', - nextStep: 'Naslednji korak', - addNextStep: 'Dodaj naslednji blok v tem delovnem toku', - selectNextStep: 'Izberi naslednji blok', - runThisStep: 'Zaženi ta korak', - checklist: 'Kontrolni seznam', - checklistTip: 'Poskrbite, da so vsi problemi rešeni pred objavo', - checklistResolved: 'Vsi problemi so rešeni', - organizeBlocks: 'Organiziraj bloke', - change: 'Spremeni', - optional: '(neobvezno)', - }, - nodes: { - common: { - outputVars: 'Izhodne spremenljivke', - insertVarTip: 'Vstavi spremenljivko', - memory: { - memory: 'Pomnjenje', - memoryTip: 'Nastavitve pomnjenja klepeta', - windowSize: 'Velikost okna', - conversationRoleName: 'Ime vloge v konverzaciji', - user: 'Predpona uporabnika', - assistant: 'Predpona pomočnika', - }, - memories: { - title: 'Pomnjenje', - tip: 'Pomnjenje klepeta', - builtIn: 'Vgrajeno', - }, - }, - start: { - required: 'obvezno', - inputField: 'Vnosno polje', - builtInVar: 'Vgrajene spremenljivke', - outputVars: { - query: 'Uporabniški vnos', - memories: { - des: 'Zgodovina konverzacije', - type: 'vrsta sporočila', - content: 'vsebina sporočila', - }, - files: 'Seznam datotek', - }, - noVarTip: 'Nastavite vnose, ki jih lahko uporabite v delovnem toku', - }, - end: { - outputs: 'Izhodi', - output: { - type: 'vrsta izhoda', - variable: 'izhoda spremenljivka', - }, - type: { - 'none': 'Brez', - 'plain-text': 'Navadno besedilo', - 'structured': 'Strukturirano', - }, - }, - answer: { - answer: 'Odgovor', - outputVars: 'Izhodne spremenljivke', - }, - llm: { - model: 'model', - variables: 'spremenljivke', - context: 'kontekst', - contextTooltip: 'Kot kontekst lahko uvozite Znanje', - notSetContextInPromptTip: 'Za omogočanje funkcije konteksta, prosimo, izpolnite kontekstno spremenljivko v POZIVU.', - prompt: 'poziv', - roleDescription: { - system: 'Podajte splošna navodila za konverzacijo', - user: 'Podajte navodila, poizvedbe ali katero koli besedilno vsebino za model', - assistant: 'Odzivi modela na uporabniška sporočila', - }, - addMessage: 'Dodaj sporočilo', - vision: 'vizija', - files: 'Datoteke', - resolution: { - name: 'Ločljivost', - high: 'Visoka', - low: 'Nizka', - }, - outputVars: { - output: 'Generirana vsebina', - usage: 'Podatki o uporabi modela', - }, - singleRun: { - variable: 'Spremenljivka', - }, - sysQueryInUser: 'sys.query v uporabniškem sporočilu je obvezno', - }, - knowledgeRetrieval: { - queryVariable: 'Poizvedbena spremenljivka', - knowledge: 'Znanje', - outputVars: { - output: 'Pridobljeni segmentirani podatki', - content: 'Segmentirana vsebina', - title: 'Segmentirani naslov', - icon: 'Segmentirana ikona', - url: 'Segmentiran URL', - metadata: 'Drugi metapodatki', - }, - }, - http: { - inputVars: 'Vnosne spremenljivke', - api: 'API', - apiPlaceholder: 'Vnesite URL, vstavite spremenljivko z tipko ‘/’', - notStartWithHttp: 'API mora začeti z http:// ali https://', - key: 'Ključ', - value: 'Vrednost', - bulkEdit: 'Serijsko urejanje', - keyValueEdit: 'Urejanje ključ-vrednost', - headers: 'Glave', - params: 'Parametri', - body: 'Telo', - outputVars: { - body: 'Vsebina odgovora', - statusCode: 'Statusna koda odgovora', - headers: 'Seznam glave odgovora v JSON', - files: 'Seznam datotek', - }, - }, - authorization: { - 'authorization': 'Avtorizacija', - 'authorizationType': 'Vrsta avtorizacije', - 'no-auth': 'Brez', - 'api-key': 'API-ključ', - 'auth-type': 'Vrsta avtorizacije', - 'basic': 'Osnovna', - 'bearer': 'Imetnik', - 'custom': 'Prilagojena', - 'api-key-title': 'API ključ', - 'header': 'Glava', - }, - insertVarPlaceholder: 'vnesite \'/\' za vstavljanje spremenljivke', - timeout: { - title: 'Časovna omejitev', - connectLabel: 'Časovna omejitev povezave', - connectPlaceholder: 'Vnesite časovno omejitev povezave v sekundah', - readLabel: 'Časovna omejitev branja', - readPlaceholder: 'Vnesite časovno omejitev branja v sekundah', - writeLabel: 'Časovna omejitev pisanja', - writePlaceholder: 'Vnesite časovno omejitev pisanja v sekundah', - }, - }, - code: { - inputVars: 'Vhodne spremenljivke', - outputVars: 'Izhodne spremenljivke', - advancedDependencies: 'Napredne odvisnosti', - advancedDependenciesTip: 'Dodajte nekaj prednaloženih odvisnosti, ki potrebujejo več časa za nalaganje ali niso privzeto vgrajene', - searchDependencies: 'Išči odvisnosti', - }, - templateTransform: { - inputVars: 'Vhodne spremenljivke', - code: 'Koda', - codeSupportTip: 'Podpira samo Jinja2', - outputVars: { - output: 'Pretvorjena vsebina', - }, - }, - ifElse: { - if: 'Če', - else: 'Sicer', - elseDescription: 'Uporablja se za definiranje logike, ki naj se izvede, ko pogoj "če" ni izpolnjen.', - and: 'in', - or: 'ali', - operator: 'Operater', - notSetVariable: 'Najprej nastavite spremenljivko', - comparisonOperator: { - 'contains': 'vsebuje', - 'not contains': 'ne vsebuje', - 'start with': 'se začne z', - 'end with': 'se konča z', - 'is': 'je', - 'is not': 'ni', - 'empty': 'je prazna', - 'not empty': 'ni prazna', - 'null': 'je null', - 'not null': 'ni null', - }, - enterValue: 'Vnesite vrednost', - addCondition: 'Dodaj pogoj', - conditionNotSetup: 'Pogoj NI nastavljen', - selectVariable: 'Izberite spremenljivko...', - }, - variableAssigner: { - title: 'Dodeli spremenljivke', - outputType: 'Vrsta izhoda', - varNotSet: 'Spremenljivka ni nastavljena', - noVarTip: 'Dodajte spremenljivke, ki jih želite dodeliti', - type: { - string: 'Niz', - number: 'Število', - object: 'Objekt', - array: 'Polje', - }, - aggregationGroup: 'Skupina za združevanje', - aggregationGroupTip: 'Omogočanje te funkcije omogoča agregatorju spremenljivk združevanje več naborov spremenljivk.', - addGroup: 'Dodaj skupino', - outputVars: { - varDescribe: 'Izhod {{groupName}}', - }, - setAssignVariable: 'Nastavi dodeljeno spremenljivko', - }, - assigner: { - 'assignedVariable': 'Dodeljena spremenljivka', - 'writeMode': 'Način pisanja', - 'writeModeTip': 'Način dodajanja: Na voljo samo za spremenljivke vrste polje.', - 'over-write': 'Prepiši', - 'append': 'Dodaj', - 'plus': 'Plus', - 'clear': 'Počisti', - 'setVariable': 'Nastavi spremenljivko', - 'variable': 'Spremenljivka', - }, - tool: { - inputVars: 'Vhodne spremenljivke', - outputVars: { - text: 'orodje je ustvarilo vsebino', - files: { - title: 'orodje je ustvarilo datoteke', - type: 'Podprta vrsta. Trenutno podpira samo slike', - transfer_method: 'Način prenosa. Vrednosti so remote_url ali local_file', - url: 'URL slike', - upload_file_id: 'ID naložene datoteke', - }, - json: 'orodje je ustvarilo json', - }, - }, - questionClassifiers: { - model: 'model', - inputVars: 'Vhodne spremenljivke', - outputVars: { - className: 'Ime razreda', - }, - class: 'Razred', - classNamePlaceholder: 'Vnesite ime razreda', - advancedSetting: 'Napredna nastavitev', - topicName: 'Ime teme', - topicPlaceholder: 'Vnesite ime teme', - addClass: 'Dodaj razred', - instruction: 'Navodilo', - instructionTip: 'Vnesite dodatna navodila, da bo klasifikator vprašanj lažje razumel, kako kategorizirati vprašanja.', - instructionPlaceholder: 'Vnesite vaše navodilo', - }, - parameterExtractor: { - inputVar: 'Vhodna spremenljivka', - extractParameters: 'Izvleči parametre', - importFromTool: 'Uvozi iz orodij', - addExtractParameter: 'Dodaj izvlečen parameter', - addExtractParameterContent: { - name: 'Ime', - namePlaceholder: 'Ime izvlečenega parametra', - type: 'Vrsta', - typePlaceholder: 'Vrsta izvlečenega parametra', - description: 'Opis', - descriptionPlaceholder: 'Opis izvlečenega parametra', - required: 'Obvezno', - requiredContent: 'Obvezno je uporabljeno samo kot referenca za sklepanja modela in ne za obvezno preverjanje izhoda parametra.', - }, - extractParametersNotSet: 'Parametri za izvleček niso nastavljeni', - instruction: 'Navodilo', - instructionTip: 'Vnesite dodatna navodila, da parameter extractor lažje razume, kako izvleči parametre.', - advancedSetting: 'Napredna nastavitev', - reasoningMode: 'Način sklepanja', - reasoningModeTip: 'Lahko izberete ustrezen način sklepanja glede na sposobnost modela za odgovore na navodila za klice funkcij ali pozive.', - isSuccess: 'Je uspeh. Pri uspehu je vrednost 1, pri neuspehu pa 0.', - errorReason: 'Razlog za napako', - }, - iteration: { - deleteTitle: 'Izbrisati vozlišče iteracije?', - deleteDesc: 'Brisanje vozlišča iteracije bo izbrisalo vsa podrejena vozlišča', - input: 'Vhod', - output: 'Izhodne spremenljivke', - iteration_one: '{{count}} iteracija', - iteration_other: '{{count}} iteracij', - currentIteration: 'Trenutna iteracija', - }, - note: { - addNote: 'Dodaj opombo', - editor: { - placeholder: 'Zapišite opombo...', - small: 'Majhno', - medium: 'Srednje', - large: 'Veliko', - bold: 'Krepko', - italic: 'Poševno', - strikethrough: 'Prečrtano', - link: 'Povezava', - openLink: 'Odpri', - unlink: 'Odstrani povezavo', - enterUrl: 'Vnesite URL...', - invalidUrl: 'Neveljaven URL', - bulletList: 'Označen seznam', - showAuthor: 'Pokaži avtorja', - }, - }, - }, - tracing: { - stopBy: 'Ustavljeno s strani {{user}}', + envPanelTitle: 'Spremenljivke okolja', + envPanelButton: 'Dodaj spremenljivko', + envDescription: 'Okoljske spremenljivke se lahko uporabljajo za shranjevanje zasebnih informacij in poverilnic. So samo za branje in jih je mogoče ločiti od DSL datoteke med izvozem.', }, chatVariable: { modal: { - type: 'Vrsta', - objectValue: 'Privzeta vrednost', - description: 'Opis', - editTitle: 'Urejanje spremenljivke pogovora', namePlaceholder: 'Ime spremenljivke', - valuePlaceholder: 'Privzeta vrednost, pustite prazno, da ni nastavljeno', - title: 'Dodajanje spremenljivke pogovora', - editInJSON: 'Urejanje v JSON', - value: 'Privzeta vrednost', - oneByOne: 'Dodajanje enega za drugim', + title: 'Dodaj spremenljivko za pogovor', + editInJSON: 'Uredi v JSON', objectKey: 'Ključ', - objectType: 'Vrsta', - arrayValue: 'Vrednost', + valuePlaceholder: 'Privzeta vrednost, pustite prazno, da je ne nastavite', + description: 'Opis', + descriptionPlaceholder: 'Opisujte spremenljivko', + type: 'Tip', + value: 'Privzeta vrednost', name: 'Ime', - descriptionPlaceholder: 'Opis spremenljivke', + arrayValue: 'Vrednost', + editTitle: 'Uredi spremenljivko pogovora', editInForm: 'Uredi v obrazcu', - addArrayValue: 'Dodajanje vrednosti', + addArrayValue: 'Dodati vrednost', + objectType: 'Tip', + oneByOne: 'Dodaj eno po eno', + objectValue: 'Privzeta vrednost', }, + updatedAt: 'Posodobljeno ob', + docLink: 'Obiščite našo dokumentacijo, da se naučite več.', + panelTitle: 'Pogovor Spremenljivke', storedContent: 'Shranjena vsebina', - updatedAt: 'Posodobljeno na', - panelTitle: 'Spremenljivke pogovora', - button: 'Dodajanje spremenljivke', - panelDescription: 'Spremenljivke pogovora se uporabljajo za shranjevanje interaktivnih informacij, ki si jih mora LLM zapomniti, vključno z zgodovino pogovorov, naloženimi datotekami, uporabniškimi nastavitvami. So branje in pisanje.', - docLink: 'Če želite izvedeti več, obiščite naše dokumente.', + panelDescription: 'Spremenljivke pogovora se uporabljajo za shranjevanje interaktivnih informacij, ki se jih LLM mora zapomniti, vključno z zgodovino pogovorov, naloženimi datotekami in uporabnikovimi nastavitvami. So branje-in-pisanje.', + button: 'Dodaj spremenljivko', }, changeHistory: { - nodeChange: 'Blokiranje spremenjeno', - placeholder: 'Ničesar še niste spremenili', - nodeDescriptionChange: 'Opis bloka je bil spremenjen', - nodePaste: 'Blokiranje lepljenja', - noteDelete: 'Opomba izbrisana', - nodeDragStop: 'Blok premaknjen', - nodeConnect: 'Blok povezan', + stepBackward_other: '{{count}} korakov nazaj', sessionStart: 'Začetek seje', - nodeDelete: 'Blokiraj izbrisane', - stepBackward_other: '{{count}} stopi nazaj', - hint: 'Namig', - noteAdd: 'Opomba dodana', - clearHistory: 'Počisti zgodovino', - stepForward_one: '{{count}} korak naprej', - stepBackward_one: '{{count}} korak nazaj', - nodeAdd: 'Blokiranje dodano', + nodeTitleChange: 'Naslov vozlišča je bil spremenjen', noteChange: 'Opomba spremenjena', - hintText: 'Dejanjem urejanja se sledi v zgodovini sprememb, ki je shranjena v napravi za čas trajanja te seje. Ta zgodovina bo izbrisana, ko zapustite urejevalnik.', - stepForward_other: '{{count}} koraki naprej', - edgeDelete: 'Blok je prekinjen.', - nodeTitleChange: 'Naslov bloka spremenjen', - nodeResize: 'Spremeni velikost bloka', title: 'Zgodovina sprememb', + noteAdd: 'Opomba dodana', + nodeAdd: 'Vozlišče dodano', + nodeDragStop: 'Vozlišče je bilo premaknjeno', + stepBackward_one: '{{count}} korak nazaj', + stepForward_other: '{{count}} korakov naprej', + nodeDelete: 'Vozlišče je bilo izbrisano', + edgeDelete: 'Vozlišče je odklopljeno', + nodeResize: 'Vozlišče je spremenjeno velikost', + hint: 'Namig', + nodeDescriptionChange: 'Opis vozlišča je bil spremenjen', + noteDelete: 'Opomba izbrisana', currentState: 'Trenutno stanje', + nodeConnect: 'Povezana vozlišča', + nodeChange: 'Vozlišče se je spremenilo', + nodePaste: 'Vozlišče prilepljeno', + clearHistory: 'Počisti zgodovino', + hintText: 'Vaša dejanja urejanja so sledena v zgodovini sprememb, ki se hrani na vaši napravi za čas trajanja te seje. Ta zgodovina bo izbrisana, ko zapustite urejevalnik.', + placeholder: 'Še niste spremenili ničesar.', + stepForward_one: '{{count}} korak naprej', }, errorMsg: { fields: { - code: 'Koda', - variableValue: 'Vrednost spremenljivke', - visionVariable: 'Spremenljivka vida', + variableValue: 'Spremenljivka Vrednost', model: 'Model', - rerankModel: 'Ponovno razvrsti model', - variable: 'Ime spremenljivke', + variable: 'Spremenljivka Ime', + code: 'Koda', + rerankModel: 'Konfiguriran model ponovne uvrstitve', + visionVariable: 'Vizijska spremenljivka', }, - invalidJson: '{{field}} je neveljaven JSON', invalidVariable: 'Neveljavna spremenljivka', - authRequired: 'Dovoljenje je potrebno', - fieldRequired: '{{field}} je obvezno', - rerankModelRequired: 'Preden vklopite Rerank Model, preverite, ali je bil model uspešno konfiguriran v nastavitvah.', + noValidTool: '{{field}} ni izbranega veljavnega orodja', toolParameterRequired: '{{field}}: parameter [{{param}}] je obvezen', - noValidTool: '{{field}} Izbrano ni veljavno orodje', + rerankModelRequired: 'Zahteva se konfigurirana model ponovnega razvrščanja.', + authRequired: 'Zahtevana je avtorizacija', + invalidJson: '{{field}} je neveljaven JSON', + fieldRequired: '{{field}} je obvezno', }, singleRun: { - startRun: 'Začni zagnati', - running: 'Tek', - testRunIteration: 'Ponovitev preskusnega zagona', - iteration: 'Ponovitev', - back: 'Hrbet', - testRun: 'Preskusni zagon', + iteration: 'Iteracija', + startRun: 'Začni zagon', loop: 'Zanka', + running: 'Tek', + testRunIteration: 'Testiranje ponovitve', + back: 'Nazaj', + testRun: 'Testna vožnja', }, tabs: { - 'blocks': 'Bloki', - 'workflowTool': 'Potek dela', - 'transform': 'Preoblikovanje', - 'question-understand': 'Vprašanje razumeti', - 'builtInTool': 'Vgrajeno', - 'allTool': 'Ves', - 'tools': 'Orodja', + 'customTool': 'Po meri', 'logic': 'Logika', - 'searchBlock': 'Iskalni blok', - 'noResult': 'Ni najdenega ujemanja', - 'customTool': 'Običaj', - 'utilities': 'Utilities', - 'searchTool': 'Orodje za iskanje', - 'agent': 'Strategija agenta', + 'tools': 'Orodja', + 'searchBlock': 'Išči vozlišče', + 'utilities': 'Komunalne storitve', 'plugin': 'Vtičnik', + 'allTool': 'Vse', + 'searchTool': 'Orodje za iskanje', + 'workflowTool': 'Delovni tok', + 'noResult': 'Ni bilo najdenih ujemanj', + 'transform': 'Pretvori', + 'blocks': 'Vozlišča', + 'question-understand': 'Vprašanje Razumevanje', + 'agent': 'Agentska strategija', }, blocks: { - 'variable-aggregator': 'Spremenljivi agregator', - 'code': 'Koda', - 'parameter-extractor': 'Ekstraktor parametrov', + 'iteration': 'Iteracija', + 'if-else': 'Če/Drugače', 'llm': 'LLM', - 'knowledge-retrieval': 'Pridobivanje znanja', - 'answer': 'Odgovoriti', - 'end': 'Konec', 'document-extractor': 'Ekstraktor dokumentov', - 'assigner': 'Dodeljevalnik spremenljivke', - 'iteration-start': 'Začetek ponovitve', - 'template-transform': 'Predloga', - 'iteration': 'Ponovitev', - 'start': 'Začetek', - 'if-else': 'IF/ELSE', - 'list-operator': 'Operater seznama', - 'http-request': 'Zahteva HTTP', - 'variable-assigner': 'Spremenljivi agregator', - 'question-classifier': 'Klasifikator vprašanj', - 'agent': 'Agent', + 'knowledge-retrieval': 'Pridobivanje znanja', + 'loop-start': 'Začetek zanke', + 'assigner': 'Dodeljevalec spremenljivk', + 'question-classifier': 'Razvrščevalec vprašanj', + 'start': 'Začni', 'loop-end': 'Izhod iz zanke', + 'http-request': 'HTTP zahteva', + 'code': 'Koda', + 'template-transform': 'Predloga', + 'answer': 'Odgovor', + 'end': 'Konec', + 'iteration-start': 'Začetek iteracije', + 'list-operator': 'Seznam operater', + 'variable-aggregator': 'Spremenljivka agregator', + 'parameter-extractor': 'Ekstraktor parametrov', 'loop': 'Zanka', - 'loop-start': 'Začetek zanke', + 'agent': 'Agent', + 'variable-assigner': 'Spremenljivka agregator', }, blocksAbout: { - 'document-extractor': 'Uporablja se za razčlenjevanje naloženih dokumentov v besedilno vsebino, ki je zlahka razumljiva LLM.', - 'list-operator': 'Uporablja se za filtriranje ali razvrščanje vsebine matrike.', - 'template-transform': 'Pretvorite podatke v niz s sintakso predloge Jinja', - 'question-classifier': 'Določite pogoje razvrščanja uporabniških vprašanj, LLM lahko določi, kako poteka pogovor na podlagi opisa klasifikacije', - 'start': 'Določanje začetnih parametrov za zagon poteka dela', - 'if-else': 'Omogoča razdelitev poteka dela na dve veji glede na pogoje if/else', - 'knowledge-retrieval': 'Omogoča poizvedovanje po besedilni vsebini, ki je povezana z uporabniškimi vprašanji iz zbirke znanja', - 'variable-assigner': 'Združite spremenljivke z več vejami v eno spremenljivko za poenoteno konfiguracijo nadaljnjih vozlišč.', - 'code': 'Izvedite kodo Python ali NodeJS za izvajanje logike po meri', - 'answer': 'Določanje vsebine odgovora v pogovoru v klepetu', - 'iteration': 'Izvedite več korakov na predmetu seznama, dokler niso prikazani vsi rezultati.', - 'http-request': 'Dovoli pošiljanje zahtev strežnika prek protokola HTTP', - 'end': 'Določanje končne in končne vrste poteka dela', - 'variable-aggregator': 'Združite spremenljivke z več vejami v eno spremenljivko za poenoteno konfiguracijo nadaljnjih vozlišč.', - 'parameter-extractor': 'Uporabite LLM za pridobivanje strukturiranih parametrov iz naravnega jezika za klicanje orodij ali zahteve HTTP.', - 'assigner': 'Vozlišče za dodeljevanje spremenljivk se uporablja za dodeljevanje vrednosti zapisljivim spremenljivkam (kot so spremenljivke pogovora).', - 'llm': 'Sklicevanje na velike jezikovne modele za odgovarjanje na vprašanja ali obdelavo naravnega jezika', - 'agent': 'Sklicevanje na velike jezikovne modele za odgovarjanje na vprašanja ali obdelavo naravnega jezika', - 'loop': 'Izvedite zanko logike, dokler ni izpolnjen pogoj za prekinitev ali dokler ni dosežena največja število ponovitev.', + 'list-operator': 'Uporabljeno za filtriranje ali razvrščanje vsebine polja.', + 'template-transform': 'Pretvori podatke v niz z uporabo Jinja predloge', + 'if-else': 'Omogoča vam, da razdelite delovni tok na dve veji na podlagi pogojev if/else.', + 'code': 'Izvedite kos Python ali NodeJS kode za izvajanje prilagojene logike.', + 'iteration': 'Izvedite več korakov na seznamu objektov, dokler niso vsi rezultati izpisani.', 'loop-end': 'Enakovredno „prekini“. Ta vozlišče nima konfiguracijskih elementov. Ko telo zanke doseže to vozlišče, zanka preneha.', + 'document-extractor': 'Uporabljeno za razčlenitev prenesenih dokumentov v besedilno vsebino, ki jo je enostavno razumeti za LLM.', + 'answer': 'Določi vsebino odgovora v pogovoru.', + 'end': 'Določite tip konca in rezultata delovnega toka', + 'knowledge-retrieval': 'Omogoča vam, da poizvedujete o besedilnih vsebinah, povezanih z vprašanji uporabnikov iz znanja.', + 'http-request': 'Dovoli pošiljanje zahtevkov strežniku prek protokola HTTP', + 'llm': 'Uporaba velikih jezikovnih modelov za odgovarjanje na vprašanja ali obdelavo naravnega jezika', + 'loop': 'Izvedite zanko logike, dokler ni izpolnjen pogoj za prekinitev ali dokler ni dosežena največja število ponovitev.', + 'question-classifier': 'Določite pogoje klasifikacije uporabniških vprašanj, LLM lahko določi, kako se pogovor razvija na podlagi opisa klasifikacije.', + 'parameter-extractor': 'Uporabite LLM za pridobivanje strukturiranih parametrov iz naravnega jezika za klice orodij ali HTTP zahtev.', + 'agent': 'Uporaba velikih jezikovnih modelov za odgovarjanje na vprašanja ali obdelavo naravnega jezika', + 'start': 'Določite začetne parametre za zagon delovnega toka', + 'variable-assigner': 'Združite večpodružinske spremenljivke v eno samo spremenljivko za enotno konfiguracijo spodnjih vozlišč.', + 'variable-aggregator': 'Združite večpodružnične spremenljivke v eno samo spremenljivko za enotno konfiguracijo spodnjih vozlišč.', }, operator: { - zoomOut: 'Pomanjšanje', - zoomTo100: 'Povečava na 100 %', + zoomOut: 'Zoomirati ven', zoomToFit: 'Povečaj, da se prilega', - zoomIn: 'Povečava', - zoomTo50: 'Povečava na 50%', + zoomIn: 'Zoom in', + zoomTo50: 'Povečaj na 50%', + zoomTo100: 'Povečaj na 100%', + }, + variableReference: { + conversationVars: 'pogovorne spremenljivke', + assignedVarsDescription: 'Dodeljene spremenljivke morajo biti spremenljivke, ki jih je mogoče pisati, na primer', + noAvailableVars: 'Ni razpoložljivih spremenljivk.', + noAssignedVars: 'Nobenih dodeljenih spremenljivk ni na voljo.', + noVarsForOperation: 'Za izbrano operacijo ni nobenih spremenljivk, ki bi jih bilo mogoče dodeliti.', }, panel: { - helpLink: 'Povezava za pomoč', - organizeBlocks: 'Organiziranje blokov', - optional: '(neobvezno)', + change: 'Spremeni', + about: 'O tem', + userInputField: 'Uporabniško vhodno polje', nextStep: 'Naslednji korak', - checklist: 'Kontrolni seznam', - runThisStep: 'Zaženite ta korak', - about: 'Približno', - selectNextStep: 'Izberite Naslednji blok', - changeBlock: 'Spremeni blok', - createdBy: 'Ustvaril', - checklistTip: 'Pred objavo se prepričajte, da so vse težave odpravljene', - userInputField: 'Uporabniško polje za vnos', - checklistResolved: 'Vse težave so odpravljene', - addNextStep: 'Dodajanje naslednjega bloka v ta potek dela', - change: 'Spremeniti', + runThisStep: 'Izvedi ta korak', + changeBlock: 'Spremeni vozlišče', + addNextStep: 'Dodajte naslednji korak v ta delovni potek', moveToThisNode: 'Premakni se na to vozlišče', + checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.', + selectNextStep: 'Izberi naslednji korak', + helpLink: 'Pomočna povezava', + checklist: 'Kontrolni seznam', + checklistResolved: 'Vse težave so rešene', + createdBy: 'Ustvarjeno z', + organizeBlocks: 'Organizirajte vozlišča', + minimize: 'Izhod iz celotnega zaslona', + maximize: 'Maksimiziraj platno', }, nodes: { common: { memory: { - conversationRoleName: 'Ime vloge pogovora', - memoryTip: 'Nastavitve pomnilnika klepeta', - assistant: 'Predpona pomočnika', - user: 'Uporabniška predpona', + user: 'Uporabniški predpon', + assistant: 'Pomagalec predpona', memory: 'Spomin', + conversationRoleName: 'Ime vloge v pogovoru', + memoryTip: 'Nastavitve spomina za klepet', windowSize: 'Velikost okna', }, memories: { tip: 'Pomnilnik klepeta', - title: 'Spomine', + title: 'Spomini', builtIn: 'Vgrajeno', }, - outputVars: 'Izhodne spremenljivke', - insertVarTip: 'Vstavi spremenljivko', errorHandle: { none: { - desc: 'Vozlišče se bo prenehalo izvajati, če pride do izjeme in ne bo obravnavano', - title: 'Nobena', + title: 'Noben', + desc: 'Vozlišče se bo prenehalo izvajati, če pride do izjeme, ki ni obravnavana.', }, defaultValue: { - output: 'Izhodna privzeta vrednost', - tip: 'Po napaki se vrne pod vrednost.', + output: 'Privzeta vrednost izhoda', + inLog: 'Napaka vozlišča, izhod po privzetih vrednostih.', title: 'Privzeta vrednost', - inLog: 'Izjema vozlišča, izhod v skladu s privzetimi vrednostmi.', - desc: 'Ko pride do napake, določite statično izhodno vsebino.', + desc: 'Ko pride do napake, določi statično vsebino izhoda.', + tip: 'Ob napaki bo vrnil spodnjo vrednost.', }, failBranch: { - title: 'Neuspešna veja', - inLog: 'Izjema vozlišča, bo samodejno izvedla neuspešno vejo. Izhod vozlišča bo vrnil vrsto napake in sporočilo o napaki ter ju posredoval navzdol.', - desc: 'Ko pride do napake, bo izvedla vejo izjeme', - customizeTip: 'Ko je aktivirana veja neuspeha, izjeme, ki jih vržejo vozlišča, ne bodo prekinile procesa. Namesto tega bo samodejno izvedel vnaprej določeno vejo neuspeha, kar vam bo omogočilo prilagodljivo zagotavljanje sporočil o napakah, poročil, popravkov ali preskakovanja dejanj.', - customize: 'Pojdite na platno, da prilagodite logiko neuspešne veje.', + title: 'Napaka veja', + customize: 'Pojdite na platno, da prilagodite logiko veje neuspeha.', + desc: 'Ko pride do napake, se bo izvedla veja izjeme.', + inLog: 'Napaka na vozlišču, samodejno se bo izvedla veja za neuspeh. Izhod vozlišča bo vrnil tip napake in sporočilo o napaki ter ju posredoval naprej.', + customizeTip: 'Ko je aktivirana veja napak, izjeme, ki jih sprožijo vozlišča, ne bodo prekinile procesa. Namesto tega bo samodejno izvedena vnaprej določena veja napak, kar vam omogoča, da prilagodljivo ponudite sporočila o napakah, poročila, popravke ali preskočite dejanja.', }, partialSucceeded: { - tip: 'V procesu so {{num}} vozlišča, ki se izvajajo nenormalno, pojdite na sledenje, da preverite dnevnike.', + tip: 'V procesu je {{num}} vozlišč, ki delujejo nenormalno, prosim, pojdite na sledenje, da preverite dnevnike.', }, - title: 'Ravnanje z napakami', - tip: 'Strategija ravnanja z izjemami, ki se sproži, ko vozlišče naleti na izjemo.', + title: 'Obvladovanje napak', + tip: 'Strategija ravnanja z izjemo, ki se sproži, ko vozlišče naleti na izjemo.', }, retry: { - retryOnFailure: 'Ponovni poskus ob neuspehu', - retryInterval: 'Interval ponovnega poskusa', - retrying: 'Ponovnim...', - retry: 'Ponoviti', - retryFailedTimes: '{{times}} ponovni poskusi niso uspeli', - retries: '{{num}} Poskusov', - times: 'Krat', - retryTimes: 'Ponovni poskus {{times}}-krat ob neuspehu', - retryFailed: 'Ponovni poskus ni uspel', retrySuccessful: 'Ponovni poskus je bil uspešen', - maxRetries: 'Največ ponovnih poskusov', - ms: 'Ms', - }, + retryFailedTimes: '{{times}} poskusi so spodleteli', + maxRetries: 'maksimalno število poskusov', + ms: 'ms', + retrying: 'Ponovno poskušam...', + times: 'časi', + retry: 'Poskusi znova', + retryFailed: 'Ponovi neuspeh', + retryOnFailure: 'poskusi znova v primeru napake', + retryInterval: 'ponovni interval', + retries: '{{num}} Poskusi', + retryTimes: 'Poskusite {{times}} krat v primeru napake', + }, + insertVarTip: 'Vstavite spremenljivko', + outputVars: 'Izhodne spremenljivke', + typeSwitch: {}, }, start: { outputVars: { memories: { - content: 'Vsebina sporočila', - des: 'Zgodovina pogovorov', - type: 'Vrsta sporočila', + des: 'Zgodovina pogovora', + type: 'vrsta sporočila', + content: 'vsebina sporočila', }, - query: 'Uporabniški vnos', files: 'Seznam datotek', + query: 'Uporabniški vnos', }, - required: 'Zahteva', - inputField: 'Vnosno polje', - noVarTip: 'Nastavitev vhodov, ki jih je mogoče uporabiti v poteku dela', + noVarTip: 'Nastavite vhodne podatke, ki jih lahko uporabite v delovnem toku.', + required: 'zahtevano', builtInVar: 'Vgrajene spremenljivke', + inputField: 'Vnosno polje', }, end: { output: { + type: 'izhodna vrsta', variable: 'izhodna spremenljivka', - type: 'Vrsta izhoda', }, type: { - 'structured': 'Strukturiran', + 'structured': 'Strukturirano', + 'none': 'Noben', 'plain-text': 'Navadno besedilo', - 'none': 'Nobena', }, - outputs: 'Izhodov', + outputs: 'Izhodi', }, answer: { - answer: 'Odgovoriti', outputVars: 'Izhodne spremenljivke', + answer: 'Odgovor', }, llm: { roleDescription: { - assistant: 'Odgovori modela na podlagi sporočil uporabnikov', - system: 'Podajte navodila na visoki ravni za pogovor', - user: 'Navedite navodila, poizvedbe ali kakršen koli besedilni vnos v model', + system: 'Dajte visoko raven navodil za pogovor.', + assistant: 'Odgovori modela so zasnovani na uporabnikovih sporočilih.', + user: 'Navedite navodila, poizvedbe ali kakršnokoli besedilne vnose modelu', }, resolution: { - low: 'Nizek', - high: 'Visok', + low: 'Nizko', + high: 'Visoko', name: 'Resolucija', }, outputVars: { + output: 'Ustvari vsebino', usage: 'Informacije o uporabi modela', - output: 'Ustvarjanje vsebine', }, singleRun: { variable: 'Spremenljivka', }, - notSetContextInPromptTip: 'Če želite omogočiti funkcijo konteksta, izpolnite kontekstno spremenljivko v PROMPT.', - sysQueryInUser: 'sys.query v sporočilu uporabnika je obvezen', - model: 'model', - files: 'Datoteke', - addMessage: 'Dodaj sporočilo', - context: 'Kontekstu', - variables: 'Spremenljivke', - prompt: 'Uren', - vision: 'vid', - contextTooltip: 'Znanje lahko uvozite kot kontekst', jsonSchema: { warningTips: { saveSchema: 'Prosimo, da dokončate urejanje trenutnega polja, preden shranite shemo.', }, - addChildField: 'Dodaj polje za otroka', + generatedResult: 'Generiran rezultat', instruction: 'Navodilo', - regenerate: 'Ponovno generiranje', - back: 'Nazaj', - generationTip: 'Lahko uporabite naravni jezik za hitro ustvarjanje JSON sheme.', - title: 'Strukturirana izhodna shema', + resetDefaults: 'Ponastavi', + promptPlaceholder: 'Opiši svoj JSON shemo...', generating: 'Generiranje JSON sheme...', - showAdvancedOptions: 'Prikaži napredne možnosti', + resultTip: 'Tukaj je generiran rezultat. Če niste zadovoljni, se lahko vrnete in spremenite svoj poziv.', promptTooltip: 'Pretvorite besedilni opis v standardizirano strukturo JSON sheme.', - generateJsonSchema: 'Generiraj JSON shemo', + addField: 'Dodaj polje', fieldNamePlaceholder: 'Ime polja', - apply: 'Prijavi se', - doc: 'Izvedite več o strukturiranem izhodu', - promptPlaceholder: 'Opiši svoj JSON shemo...', - generatedResult: 'Generiran rezultat', import: 'Uvoz iz JSON', - generate: 'Generirati', - resultTip: 'Tukaj je generiran rezultat. Če niste zadovoljni, se lahko vrnete in spremenite svoj poziv.', - stringValidations: 'Preverjanje nizov', + generationTip: 'Lahko uporabite naravni jezik za hitro ustvarjanje JSON sheme.', + back: 'Nazaj', descriptionPlaceholder: 'Dodajte opis', + generate: 'Generirati', + doc: 'Izvedite več o strukturiranem izhodu', + title: 'Strukturirana izhodna shema', required: 'zahtevano', - addField: 'Dodaj polje', - resetDefaults: 'Ponastavi', + apply: 'Uporabi', + generateJsonSchema: 'Generiraj JSON shemo', + addChildField: 'Dodaj polje za otroka', + showAdvancedOptions: 'Prikaži napredne možnosti', + stringValidations: 'Preverjanje nizov', + regenerate: 'Ponovno generiranje', }, + prompt: 'ukaz', + sysQueryInUser: 'vprašanje v uporabniškem sporočilu je obvezno', + notSetContextInPromptTip: 'Da omogočite funkcijo konteksta, prosimo izpolnite spremenljivko konteksta v PROMPT.', + contextTooltip: 'Lahko uvažate znanje kot kontekst', + variables: 'spremenljivke', + files: 'Datoteke', + model: 'model', + context: 'kontekst', + addMessage: 'Dodaj sporočilo', + vision: 'vizija', }, knowledgeRetrieval: { outputVars: { - title: 'Segmentirani naslov', - url: 'Segmentirani URL', - output: 'Pridobivanje segmentiranih podatkov', + title: 'Segmentirana naslov', + url: 'Segmentirana URL', icon: 'Segmentirana ikona', - metadata: 'Drugi metapodatki', content: 'Segmentirana vsebina', + metadata: 'Drug metapodatki', + output: 'Podatki o segmentaciji iskanja', }, - queryVariable: 'Spremenljivka poizvedbe', - knowledge: 'Znanje', metadata: { options: { disabled: { @@ -903,452 +472,485 @@ const translation = { subTitle: 'Ne omogočanje filtriranja metapodatkov', }, automatic: { - desc: 'Samodejno ustvarite filtrirne pogoje za metapodatke na podlagi spremenljivke poizvedbe', - subTitle: 'Samodejno ustvarite filtrirne pogoje za metapodatke na podlagi uporabniškega poizvedovanja.', title: 'Samodejno', + subTitle: 'Samodejno ustvarite filtrirne pogoje za metapodatke na podlagi uporabniškega poizvedovanja.', + desc: 'Samodejno ustvarite filtrirne pogoje za metapodatke na podlagi spremenljivke poizvedbe', }, manual: { - title: 'Ročno', subTitle: 'Ročno dodajte pogoje za filtriranje metapodatkov', + title: 'Ročno', }, }, panel: { title: 'Pogoji za filtriranje metapodatkov', - search: 'Išči metapodatke', + conditions: 'Pogoji', placeholder: 'Vnesite vrednost', + search: 'Išči metapodatke', select: 'Izberi spremenljivko...', - conditions: 'Pogoji', datePlaceholder: 'Izberi čas...', add: 'Dodaj pogoj', }, title: 'Filtriranje metapodatkov', }, + queryVariable: 'Vprašanje spremenljivka', + knowledge: 'Znanje', }, http: { outputVars: { - headers: 'JSON seznama glav odgovorov', - body: 'Vsebina odgovora', files: 'Seznam datotek', - statusCode: 'Koda stanja odgovora', + body: 'Vsebina odziva', + headers: 'Seznam odzivnih glav JSON', + statusCode: 'Statusna koda odgovora', }, authorization: { - 'authorization': 'Dovoljenje', - 'header': 'Glava', - 'bearer': 'Nosilec', + 'no-auth': 'Noben', + 'custom': 'Po meri', + 'header': 'Naslov', + 'bearer': 'Nosilac', 'api-key-title': 'API ključ', - 'basic': 'Osnoven', - 'no-auth': 'Nobena', - 'custom': 'Običaj', - 'authorizationType': 'Vrsta dovoljenja', - 'auth-type': 'Vrsta preverjanja pristnosti', - 'api-key': 'Ključ API-ja', + 'authorization': 'Avtorizacija', + 'api-key': 'API-kljuc', + 'basic': 'Osnovno', + 'auth-type': 'Vrsta avtentikacije', + 'authorizationType': 'Vrsta pooblastila', }, timeout: { - readPlaceholder: 'Vnos časovne omejitve branja v sekundah', - writePlaceholder: 'Vnesite časovno omejitev pisanja v sekundah', - writeLabel: 'Časovna omejitev pisanja', - connectLabel: 'Časovna omejitev povezave', - title: 'Timeout', readLabel: 'Časovna omejitev branja', - connectPlaceholder: 'Vnos časovne omejitve povezave v sekundah', + title: 'Časovna omejitev', + readPlaceholder: 'Vnesite časovne omejitve za branje v sekundah', + connectPlaceholder: 'Vnesite časovne omejitve povezave v sekundah', + connectLabel: 'Čas povezave je potekel', + writePlaceholder: 'Vnesite časovne omejitve za pisanje v sekundah', + writeLabel: 'Časovna omejitev pisanja', + }, + curl: { + title: 'Uvozi iz cURL', + placeholder: 'Prilepite cURL niz tukaj', }, - value: 'Vrednost', - key: 'Ključ', - notStartWithHttp: 'API se mora začeti z http:// ali https://', body: 'Telo', - type: 'Vrsta', inputVars: 'Vhodne spremenljivke', - bulkEdit: 'Urejanje v velikem obsegu', - insertVarPlaceholder: 'vnesite "/" za vstavljanje spremenljivke', + apiPlaceholder: 'Vnesite URL, vtipkajte \'/\' in vstavite spremenljivko.', api: 'API', + extractListPlaceholder: 'Vnesite indeks seznamske postavke, vnesite \'/\' za vstavitev spremenljivke', + key: 'Ključ', + binaryFileVariable: 'Dvojiška datoteka spremenljivka', + notStartWithHttp: 'API se mora začeti z http:// ali https://', keyValueEdit: 'Urejanje ključ-vrednost', - binaryFileVariable: 'Spremenljivka binarne datoteke', - headers: 'Glave', - apiPlaceholder: 'Vnesite URL, vnesite \'/\' vstavi spremenljivko', - extractListPlaceholder: 'Vnesite indeks elementa seznama, vnesite \'/\' vstavi spremenljivko', - params: 'Params', - curl: { - title: 'Uvoz iz cURL', - placeholder: 'Tukaj prilepite niz cURL', + bulkEdit: 'Masovno urejanje', + type: 'Tip', + headers: 'Naslovi', + value: 'Vrednost', + params: 'Parametri', + insertVarPlaceholder: 'vnesite \'/\' za vstavljanje spremenljivke', + verifySSL: { + title: 'Preverite SSL certifikat', + warningTooltip: 'Onemogočanje preverjanja SSL ni priporočljivo za proizvodna okolja. To bi se moralo uporabljati le pri razvoju ali testiranju, saj povezavo izpostavi varnostnim grožnjam, kot so napadi človek-v-sredini.', }, }, code: { - inputVars: 'Vhodne spremenljivke', - outputVars: 'Izhodne spremenljivke', - searchDependencies: 'Odvisnosti iskanja', - advancedDependenciesTip: 'Tukaj dodajte nekaj vnaprej naloženih odvisnosti, ki trajajo dlje časa ali niso privzeto vgrajene', + searchDependencies: 'Išči odvisnosti', advancedDependencies: 'Napredne odvisnosti', + outputVars: 'Izhodne spremenljivke', + inputVars: 'Vhodne spremenljivke', + advancedDependenciesTip: 'Dodajte nekaj vnaprej naloženih odvisnosti, ki potrebujejo več časa za obdelavo ali niso privzete vgrajene.', }, templateTransform: { outputVars: { - output: 'Preoblikovana vsebina', + output: 'Transformirana vsebina', }, + codeSupportTip: 'Podpira samo Jinja2', code: 'Koda', inputVars: 'Vhodne spremenljivke', - codeSupportTip: 'Podpira samo Jinja2', }, ifElse: { comparisonOperator: { - 'all of': 'vse', - 'is not': 'ni', - 'not empty': 'ni prazen', - 'start with': 'Začnite z', - 'is': 'Je', - 'null': 'je nična', - 'not exists': 'ne obstaja', - 'contains': 'Vsebuje', - 'empty': 'je prazen', - 'exists': 'Obstaja', - 'in': 'v', - 'not contains': 'ne vsebuje', - 'end with': 'Končaj z', + 'all of': 'vse od', 'not in': 'ni v', - 'not null': 'ni nična', + 'in': 'v', + 'null': 'je nič', 'after': 'po', + 'is': 'je', + 'not exists': 'ne obstaja', + 'empty': 'je prazno', + 'is not': 'ni', + 'start with': 'začeti z', + 'not empty': 'ni prazen', 'before': 'pred', + 'end with': 'končati z', + 'not contains': 'ne vsebuje', + 'contains': 'vsebuje', + 'exists': 'obstaja', + 'not null': 'ni ničelno', }, optionName: { - video: 'Video', - doc: 'Doc', - audio: 'Avdio', - image: 'Podoba', - url: 'Spletni naslov', localUpload: 'Lokalno nalaganje', + video: 'Video', + url: 'URL', + image: 'Slika', + doc: 'Dokument', + audio: 'Zvočni zapis', }, + addCondition: 'Dodaj pogoj', + selectVariable: 'Izberi spremenljivko...', + or: 'ali', + if: 'Če', and: 'in', - else: 'Drugega', + else: 'Drugje', + notSetVariable: 'Prosim, najprej nastavite spremenljivko.', enterValue: 'Vnesite vrednost', - elseDescription: 'Uporablja se za določanje logike, ki jo je treba izvesti, ko pogoj if ni izpolnjen.', - addCondition: 'Dodajanje pogoja', - if: 'Če', - select: 'Izbrati', - selectVariable: 'Izberite spremenljivko ...', - conditionNotSetup: 'Pogoj NI nastavljen', + elseDescription: 'Uporabljeno za opredelitev logike, ki se izvede, ko pogoj if ni izpolnjen.', addSubVariable: 'Podspremenljivka', - notSetVariable: 'Prosimo, najprej nastavite spremenljivko', - operator: 'Operaterja', - or: 'ali', - condition: 'Pogoji', + conditionNotSetup: 'Pogoji NISO nastavljeni', + operator: 'Operater', + select: 'Izberite', }, variableAssigner: { type: { - string: 'Niz', object: 'Predmet', - array: 'Matrika', - number: 'Številka', + string: 'Niz', + number: 'Število', + array: 'Množica', }, outputVars: { varDescribe: '{{groupName}} izhod', }, - addGroup: 'Dodajanje skupine', - outputType: 'Vrsta izhoda', - title: 'Dodeljevanje spremenljivk', - noVarTip: 'Seštevanje spremenljivk, ki jih je treba dodeliti', - aggregationGroupTip: 'Če omogočite to funkcijo, lahko združevalnik spremenljivk združi več naborov spremenljivk.', - aggregationGroup: 'Združevalna skupina', varNotSet: 'Spremenljivka ni nastavljena', - setAssignVariable: 'Nastavitev spremenljivke dodelitve', + title: 'Dodelite spremenljivke', + noVarTip: 'Dodajte spremenljivke, ki jih je treba dodeliti.', + aggregationGroup: 'Agregacijska skupina', + outputType: 'Vrsta izhoda', + addGroup: 'Dodaj skupino', + setAssignVariable: 'Določite spremenljivko', + aggregationGroupTip: 'Omogočanje te funkcije omogoča agregatorju spremenljivk, da združi več skupin spremenljivk.', }, assigner: { - 'writeMode': 'Način pisanja', - 'plus': 'Plus', - 'variable': 'Spremenljivka', - 'clear': 'Jasen', - 'append': 'Dodaj', - 'assignedVariable': 'Dodeljena spremenljivka', - 'setVariable': 'Nastavi spremenljivko', - 'over-write': 'Prepisati', - 'writeModeTip': 'Način dodajanja: Na voljo samo za spremenljivke polja.', 'operations': { - '+=': '+=', - 'overwrite': 'Prepisati', - '*=': '*=', - 'extend': 'Razširiti', + 'set': 'Nabor', 'append': 'Dodaj', - '-=': '-=', - 'title': 'Operacija', '/=': '/=', - 'set': 'Nastaviti', - 'clear': 'Jasen', - 'over-write': 'Prepisati', - 'remove-last': 'Odstrani zadnje', + 'over-write': 'Prepiši', + '*=': '*=', 'remove-first': 'Odstrani prvi', + 'remove-last': 'Odstrani zadnje', + '-=': '-=', + '+=': '+=', + 'extend': 'Razširi', + 'clear': 'Jasno', + 'overwrite': 'Prepiši', + 'title': 'Operacija', }, + 'clear': 'Jasno', + 'plus': 'Plus', + 'noAssignedVars': 'Nobenih dodeljenih spremenljivk ni na voljo.', 'variables': 'Spremenljivke', - 'selectAssignedVariable': 'Izberite dodeljeno spremenljivko ...', - 'assignedVarsDescription': 'Dodeljene spremenljivke morajo biti zapisljive, kot so spremenljivke pogovora.', - 'noVarTip': 'Kliknite gumb »+«, da dodate spremenljivke', - 'noAssignedVars': 'Ni razpoložljivih dodeljenih spremenljivk', + 'assignedVariable': 'Dodeljena spremenljivka', + 'writeMode': 'Načrtovanje pisanja', + 'setParameter': 'Nastavite parameter...', + 'writeModeTip': 'Način dodajanja: Na voljo samo za spremenljivke tipa tabel.', + 'over-write': 'Prepiši', + 'append': 'Dodaj', 'varNotSet': 'Spremenljivka NI nastavljena', - 'setParameter': 'Nastavi parameter ...', + 'noVarTip': 'Kliknite na gumb " + " za dodajanje spremenljivk', + 'variable': 'Spremenljivka', + 'assignedVarsDescription': 'Dodeljene spremenljivke morajo biti spremenljivke, ki jih je mogoče pisati, kot so spremenljivke za pogovor.', + 'setVariable': 'Nastavi spremenljivko', + 'selectAssignedVariable': 'Izberite dodeljeno spremenljivko...', }, tool: { outputVars: { files: { - transfer_method: 'Način prenosa. Vrednost je remote_url ali local_file', - upload_file_id: 'Naloži ID datoteke', - type: 'Vrsta podpore. Zdaj podpiramo samo sliko', + transfer_method: 'Metoda prenosa. Vrednost je remote_url ali local_file.', + title: 'orodja ustvarjena datoteke', + upload_file_id: 'Naložite ID datoteke', url: 'URL slike', - title: 'Datoteke, ustvarjene z orodjem', + type: 'Vrsta podpore. Zdaj podpiramo samo slike.', }, - json: 'JSON, ustvarjen z orodjem', - text: 'Vsebina, ustvarjena z orodjem', + text: 'vsebina, ki jo je generiral orodje', + json: 'orodje je ustvarilo json', }, inputVars: 'Vhodne spremenljivke', - toAuthorize: 'Za odobritev', authorize: 'Pooblasti', }, questionClassifiers: { outputVars: { className: 'Ime razreda', + usage: 'Informacije o uporabi modela', }, instruction: 'Navodilo', - classNamePlaceholder: 'Napišite ime svojega razreda', - addClass: 'Dodajanje razreda', - instructionPlaceholder: 'Napišite navodila', - topicName: 'Ime teme', - topicPlaceholder: 'Napišite ime teme', + addClass: 'Dodaj razred', class: 'Razred', - advancedSetting: 'Napredne nastavitve', model: 'model', + topicPlaceholder: 'Napišite ime svoje teme', + topicName: 'Ime teme', + instructionTip: 'Vnesite dodatna navodila, ki bodo pomagala klasifikatorju vprašanj bolje razumeti, kako kategorizirati vprašanja.', inputVars: 'Vhodne spremenljivke', - instructionTip: 'Vnesite dodatna navodila, ki bodo klasifikatorju vprašanj pomagala bolje razumeti, kako kategorizirati vprašanja.', + classNamePlaceholder: 'Napiši ime svoje razredi', + advancedSetting: 'Napredno nastavitev', + instructionPlaceholder: 'Napišite svoje navodilo', }, parameterExtractor: { addExtractParameterContent: { + required: 'Zahtevano', description: 'Opis', - typePlaceholder: 'Vrsta parametra izvlečka', - requiredContent: 'Zahtevano se uporablja samo kot referenca za sklepanje modela in ne za obvezno validacijo izhodnega parametra.', - required: 'Zahteva', - type: 'Vrsta', - namePlaceholder: 'Izvleček imena parametra', - descriptionPlaceholder: 'Opis parametra izvlečka', name: 'Ime', + descriptionPlaceholder: 'Izvleči opis parametra', + namePlaceholder: 'Izvleči ime parametra', + type: 'Tip', + typePlaceholder: 'Izvleči vrsto parametra', + requiredContent: 'Zahtevano se uporablja le kot referenca za sklepanje modela in ne kot obvezno validacijo izhodnih parametrov.', }, - isSuccess: 'Je uspeh.Pri uspehu je vrednost 1, pri neuspehu je vrednost 0.', - addExtractParameter: 'Dodajanje parametra izvlečka', + extractParameters: 'Izvleči parametre', + instruction: 'Navodilo', + instructionTip: 'Vnesite dodatna navodila, da pomagate izvleku parametrov razumeti, kako izvleči parametre.', + reasoningMode: 'Način razmišljanja', importFromTool: 'Uvoz iz orodij', - reasoningModeTip: 'Izberete lahko ustrezen način sklepanja glede na sposobnost modela, da se odzove na navodila za klicanje funkcij ali pozive.', + advancedSetting: 'Napredno nastavitev', + addExtractParameter: 'Dodaj parameter za ekstrakcijo', + extractParametersNotSet: 'Parameterji za ekstrakcijo niso nastavljeni', inputVar: 'Vhodna spremenljivka', - advancedSetting: 'Napredne nastavitve', - errorReason: 'Razlog za napako', - reasoningMode: 'Način sklepanja', - instruction: 'Navodilo', - instructionTip: 'Vnesite dodatna navodila, ki bodo ekstraktorju parametrov pomagala razumeti, kako izvleči parametre.', - extractParametersNotSet: 'Izvleček parametrov ni nastavljen', - extractParameters: 'Izvleček parametrov', + outputVars: { + isSuccess: 'Ali je uspeh. Na uspehu je vrednost 1, na neuspehu je vrednost 0.', + errorReason: 'Razlog za napako', + usage: 'Informacije o uporabi modela', + }, + reasoningModeTip: 'Lahko izberete ustrezen način razmišljanja glede na sposobnost modela, da se odzove na navodila za klic funkcij ali pozive.', }, iteration: { ErrorMethod: { - continueOnError: 'Nadaljuj ob napaki', - removeAbnormalOutput: 'Odstranite nenormalen izhod', - operationTerminated: 'Prekinjena', + operationTerminated: 'Prekinjeno', + removeAbnormalOutput: 'Odstrani nenavadne izhode', + continueOnError: 'Nadaljuj naprej kljub napaki', }, + errorResponseMethod: 'Metoda odziva napake', + parallelModeEnableTitle: 'Paralelni način vključen', output: 'Izhodne spremenljivke', - parallelMode: 'Vzporedni način', - MaxParallelismTitle: 'Največji vzporednost', - errorResponseMethod: 'Način odziva na napako', - parallelModeEnableDesc: 'V vzporednem načinu opravila v iteracijah podpirajo vzporedno izvajanje. To lahko konfigurirate na plošči z lastnostmi na desni.', - error_one: '{{štetje}} Napaka', + MaxParallelismTitle: 'Maksimalno paralelizem', + currentIteration: 'Trenutna iteracija', + error_other: '{{count}} Napak', + comma: ',', + iteration_one: '{{count}} Iteracija', + parallelMode: 'Paralelni način', + error_one: '{{count}} Napaka', + deleteTitle: 'Izbriši vozlišče ponovitve?', + iteration_other: '{{count}} ponovitev', + input: 'Vnos', + answerNodeWarningDesc: 'Opozorilo o paralelnem načinu: Odgovorni vozli, dodelitve spremenljivk v pogovorih in trajne operacije branja/pisanja znotraj iteracij lahko povzročijo izjeme.', + parallelModeUpper: 'PARALELNO MODE', + MaxParallelismDesc: 'Največje paralelizem se uporablja za nadzorovanje števila nalog, ki se izvajajo hkrati v eni iteraciji.', + deleteDesc: 'Izbris iteracijskega vozlišča bo izbrisal vsa otroška vozlišča.', + parallelModeEnableDesc: 'V vzporednem načinu naloge znotraj iteracij podpirajo vzporedno izvajanje. To lahko nastavite v razdelku lastnosti na desni strani.', + parallelPanelDesc: 'V paralelnem načinu naloge v iteraciji podpirajo parallelno izvajanje.', + }, + loop: { + ErrorMethod: { + removeAbnormalOutput: 'Odstrani nenavadne izhode', + operationTerminated: 'Prekinjeno', + continueOnError: 'Nadaljuj naprej kljub napaki', + }, + loop_one: '{{count}} Zanka', + loop_other: '{{count}} Zavoji', + input: 'Vnos', + errorResponseMethod: 'Metoda odziva napake', + output: 'Izhodna spremenljivka', + loopMaxCount: 'Maksimalno število zank', + loopVariables: 'Zanke Spremenljivke', comma: ',', - parallelModeUpper: 'VZPOREDNI NAČIN', - parallelModeEnableTitle: 'Vzporedni način omogočen', - currentIteration: 'Trenutna ponovitev', - error_other: '{{štetje}} Napake', - input: 'Vhodni', - deleteTitle: 'Izbrisati iteracijsko vozlišče?', - parallelPanelDesc: 'V vzporednem načinu opravila v iteraciji podpirajo vzporedno izvajanje.', - deleteDesc: 'Če izbrišete iteracijsko vozlišče, boste izbrisali vsa podrejena vozlišča', - iteration_other: '{{štetje}} Ponovitev', - answerNodeWarningDesc: 'Opozorilo vzporednega načina: Vozlišča za odgovore, dodelitve spremenljivk pogovora in trajne operacije branja / pisanja v iteracijah lahko povzročijo izjeme.', - MaxParallelismDesc: 'Največja vzporednost se uporablja za nadzor števila nalog, ki se izvajajo hkrati v eni ponovitvi.', - iteration_one: '{{štetje}} Ponovitev', + currentLoop: 'Trenutni obrat', + currentLoopCount: 'Trenutno število zank: {{count}}', + deleteTitle: 'Izbriši vozlišče zanke?', + loopNode: 'Ciklični vozlišče', + inputMode: 'Vnosni način', + variableName: 'Spremenljivka Ime', + exitConditionTip: 'Vozić potrebuje vsaj eno izhodno pogoj.', + finalLoopVariables: 'Končne zanke spremenljivke', + deleteDesc: 'Izbris vozlišča zanke bo odstranil vse otroške vozlišča.', + breakCondition: 'Pogoji za prekinitev zanke', + error_one: '{{count}} Napaka', + error_other: '{{count}} Napak', + setLoopVariables: 'Nastavite spremenljivke znotraj obsega zanke', + totalLoopCount: 'Skupno število zank: {{count}}', + initialLoopVariables: 'Začetne spremenljivke zanke', + breakConditionTip: 'Lahko se sklicujete le na spremenljivke znotraj zank z zaključnimi pogoji in pogovorne spremenljivke.', + loopMaxCountError: 'Prosimo, vnesite veljavno največje število ponovitev, ki mora biti med 1 in {{maxCount}}', }, note: { editor: { - medium: 'Srednja', - openLink: 'Odprt', - showAuthor: 'Pokaži avtorja', - bold: 'Smel', - strikethrough: 'Prečrtano', + bold: 'Poudarjeno', + medium: 'Srednje', large: 'Velik', link: 'Povezava', - enterUrl: 'Vnesite URL ...', + enterUrl: 'Vnesite URL...', small: 'Majhen', - italic: 'Ležeče', - invalidUrl: 'Neveljaven URL', - unlink: 'Prekini povezavo', - placeholder: 'Napišite svojo opombo ...', - bulletList: 'Seznam oznak', + bulletList: 'Seznam s puščicami', + unlink: 'Odstrani povezavo', + italic: 'Italika', + placeholder: 'Napiši svojo opombo...', + openLink: 'Odprto', + showAuthor: 'Prikaži avtorja', + strikethrough: 'Prečrtano', + invalidUrl: 'Nesprejemljiv URL', }, addNote: 'Dodaj opombo', }, docExtractor: { outputVars: { - text: 'Izvlečeno besedilo', + text: 'Izvlečene besedilo', }, + supportFileTypes: 'Podpora za vrste datotek: {{types}}.', + learnMore: 'Nauči se več', inputVar: 'Vhodna spremenljivka', - learnMore: 'Izvedi več', - supportFileTypes: 'Podporne vrste datotek: {{types}}.', }, listFilter: { outputVars: { - result: 'Rezultat filtriranja', first_record: 'Prvi zapis', last_record: 'Zadnji zapis', + result: 'Filtriraj rezultat', }, - extractsCondition: 'Ekstrahiranje elementa N', - selectVariableKeyPlaceholder: 'Izberite ključ podspremenljivke', - asc: 'ASC', - orderBy: 'Naročite po', - filterCondition: 'Pogoj filtra', filterConditionKey: 'Ključ pogoja filtra', + asc: 'ASC', + filterConditionComparisonOperator: 'Operator za primerjavo filtrovanja pogojev', + selectVariableKeyPlaceholder: 'Izberi podključ spremenljivke', + limit: 'Najboljši N', + filterConditionComparisonValue: 'Vrednost pogoja filtra', desc: 'DESC', - limit: 'Vrh N', - filterConditionComparisonOperator: 'Operator za primerjavo pogojev filtra', inputVar: 'Vhodna spremenljivka', - filterConditionComparisonValue: 'Vrednost pogoja filtra', + orderBy: 'Naroči po', + extractsCondition: 'Izvleči N predmet', + filterCondition: 'Filtrirni pogoj', }, agent: { strategy: { - configureTipDesc: 'Po konfiguraciji agentske strategije bo to vozlišče samodejno naložilo preostale konfiguracije. Strategija bo vplivala na mehanizem sklepanja z orodji v več korakih.', - tooltip: 'Različne agentske strategije določajo, kako sistem načrtuje in izvaja klice orodij v več korakih', + configureTip: 'Prosimo, konfigurirajte agentno strategijo.', + selectTip: 'Izberite agencijsko strategijo', + searchPlaceholder: 'Išči agentno strategijo', + label: 'Agentična strategija', shortLabel: 'Strategija', - configureTip: 'Prosimo, konfigurirajte agentsko strategijo.', - searchPlaceholder: 'Strategija iskalnega agenta', - label: 'Agentska strategija', - selectTip: 'Izberite agentsko strategijo', + configureTipDesc: 'Po nastavitvi agentne strategije bo ta vozlišče samodejno naložilo preostale nastavitve. Strategija bo vplivala na mehanizem večstopenjskega razmišljanja o orodju.', + tooltip: 'Različne agentne strategije določajo, kako sistem načrtuje in izvaja večstopenjske klice orodij.', }, pluginInstaller: { installing: 'Namestitev', - install: 'Namestiti', + install: 'Namestite', }, modelNotInMarketplace: { - desc: 'Ta model je nameščen iz lokalnega skladišča ali skladišča GitHub. Uporabite po namestitvi.', title: 'Model ni nameščen', manageInPlugins: 'Upravljanje v vtičnikih', + desc: 'Ta model je nameščen iz lokalnega ali GitHub repozitorija. Prosimo, uporabite ga po namestitvi.', }, modelNotSupport: { - descForVersionSwitch: 'Nameščena različica vtičnika ne zagotavlja tega modela. Kliknite, če želite preklopiti med različico.', - title: 'Nepodprt model', - desc: 'Nameščena različica vtičnika ne zagotavlja tega modela.', + title: 'Nepodprti model', + desc: 'V različici vtičnika, ki je nameščena, ta model ni na voljo.', + descForVersionSwitch: 'Nameščena različica vtičnika ne podpira tega modela. Kliknite za preklop na drugo različico.', }, modelSelectorTooltips: { - deprecated: 'Ta model je zastarel', + deprecated: 'Ta model je zastarelo', }, outputVars: { files: { + type: 'Vrsta podpore. Zdaj podpiramo samo slike.', + upload_file_id: 'Naložite ID datoteke', + title: 'datoteke, ki jih je ustvaril agent', url: 'URL slike', - title: 'Datoteke, ki jih ustvari agent', - type: 'Vrsta podpore. Zdaj podpiramo samo sliko', - upload_file_id: 'Naloži ID datoteke', - transfer_method: 'Način prenosa. Vrednost je remote_url ali local_file', + transfer_method: 'Metoda prenosa. Vrednost je remote_url ali local_file.', }, - json: 'JSON, ustvarjen z agentom', - text: 'Vsebina, ki jo ustvari agent', + json: 'agent generiran json', + text: 'vsebina, ki jo je ustvaril agent', }, checkList: { strategyNotSelected: 'Strategija ni izbrana', }, installPlugin: { - cancel: 'Odpovedati', - changelog: 'Dnevnik sprememb', - install: 'Namestiti', - title: 'Namesti vtičnik', - desc: 'O namestitvi naslednjega vtičnika', - }, - strategyNotSet: 'Agentska strategija ni določena', - modelNotSelected: 'Model ni izbran', - pluginNotInstalled: 'Ta vtičnik ni nameščen', - toolNotAuthorizedTooltip: '{{orodje}} Ni pooblaščeno', - toolbox: 'Orodjarni', - tools: 'Orodja', + desc: 'Namestitev naslednjega vtičnika', + title: 'Namestite vtičnik', + changelog: 'Zapis sprememb', + cancel: 'Prekliči', + install: 'Namestite', + }, + toolbox: 'delovna orodja', + configureModel: 'Konfiguriraj Model', toolNotInstallTooltip: '{{tool}} ni nameščen', + pluginNotInstalled: 'Ta vtičnik ni nameščen', strategyNotInstallTooltip: '{{strategy}} ni nameščen', - modelNotInstallTooltip: 'Ta model ni nameščen', - pluginNotFoundDesc: 'Ta vtičnik je nameščen iz GitHuba. Prosimo, pojdite na Vtičniki za ponovno namestitev', - maxIterations: 'Največje število ponovitev', - notAuthorized: 'Ni pooblaščeno', + modelNotInstallTooltip: 'Ta model ni nameščen.', model: 'model', - learnMore: 'Izvedi več', + maxIterations: 'Maksimalne iteracije', + notAuthorized: 'Nimam dovoljenja', + modelNotSelected: 'Model ni izbran', + learnMore: 'Nauči se več', unsupportedStrategy: 'Nepodprta strategija', - strategyNotFoundDescAndSwitchVersion: 'Nameščena različica vtičnika ne zagotavlja te strategije. Kliknite, če želite preklopiti med različico.', - strategyNotFoundDesc: 'Nameščena različica vtičnika ne zagotavlja te strategije.', - configureModel: 'Konfiguracija modela', - pluginNotInstalledDesc: 'Ta vtičnik je nameščen iz GitHuba. Prosimo, pojdite na Vtičniki za ponovno namestitev', + pluginNotFoundDesc: 'Ta vtičnik je nameščen iz GitHuba. Prosimo, da greste v vtičnike in ga ponovo namestite.', + tools: 'Orodja', + strategyNotFoundDesc: 'V različici vtičnika, ki je nameščena, ta strategija ni zagotovljena.', linkToPlugin: 'Povezava do vtičnikov', - }, - loop: { - ErrorMethod: { - operationTerminated: 'Prekinjeno', - continueOnError: 'Nadaljuj ob napaki', - removeAbnormalOutput: 'Odstrani nenavadne izhode', - }, - input: 'Vnos', - inputMode: 'Vnosni način', - errorResponseMethod: 'Metoda odziva napake', - setLoopVariables: 'Nastavite spremenljivke znotraj obsega zanke', - output: 'Izhodna spremenljivka', - loop_one: '{{count}} Zanka', - exitConditionTip: 'Vozić potrebuje vsaj eno izhodno pogoj.', - loopMaxCount: 'Maksimalno število zank', - deleteDesc: 'Izbris vozlišča zanke bo odstranil vse otroške vozlišča.', - comma: ',', - loop_other: '{{count}} Zavoji', - currentLoop: 'Trenutni obrat', - variableName: 'Spremenljivka Ime', - deleteTitle: 'Izbriši vozlišče zanke?', - error_one: '{{count}} Napaka', - totalLoopCount: 'Skupno število zank: {{count}}', - initialLoopVariables: 'Začetne spremenljivke zanke', - currentLoopCount: 'Trenutno število zank: {{count}}', - loopNode: 'Ciklični vozlišče', - loopVariables: 'Zanke Spremenljivke', - breakConditionTip: 'Lahko se sklicujete le na spremenljivke znotraj zank z zaključnimi pogoji in pogovorne spremenljivke.', - breakCondition: 'Pogoji za prekinitev zanke', - finalLoopVariables: 'Končne zanke spremenljivke', - error_other: '{{count}} Napak', - loopMaxCountError: 'Prosimo, vnesite veljavno največje število ponovitev, ki mora biti med 1 in {{maxCount}}', + strategyNotSet: 'Agentična strategija ni nastavljena', + toolNotAuthorizedTooltip: '{{tool}} Ni pooblaščen', + strategyNotFoundDescAndSwitchVersion: 'Nameščena različica vtičnika ne podpira te strategije. Kliknite za preklop na drugo različico.', + pluginNotInstalledDesc: 'Ta vtičnik je nameščen iz GitHuba. Prosimo, da greste v vtičnike in ga ponovo namestite.', }, }, - variableReference: { - noVarsForOperation: 'Spremenljivk ni na voljo za dodelitev z izbrano operacijo.', - conversationVars: 'Spremenljivke pogovora', - noAssignedVars: 'Ni razpoložljivih dodeljenih spremenljivk', - noAvailableVars: 'Ni spremenljivk, ki so na voljo', - assignedVarsDescription: 'Dodeljene spremenljivke morajo biti zapisljive, kot so:', + tracing: { + stopBy: 'Ohranjaj se pri {{user}}', }, versionHistory: { filter: { all: 'Vse', - empty: 'Ni najdene zgodovine različic, ki bi se ujemala.', - onlyShowNamedVersions: 'Prikaži samo poimenovane različice', reset: 'Ponastavi filter', + onlyShowNamedVersions: 'Prikaži samo poimenovane različice', onlyYours: 'Samo tvoje', + empty: 'Ni najdene zgodovine različic, ki bi se ujemala.', }, editField: { - titleLengthLimit: 'Naslov ne sme presegati {{limit}} znakov', title: 'Naslov', + titleLengthLimit: 'Naslov ne sme presegati {{limit}} znakov', releaseNotesLengthLimit: 'Opombe o različici ne smejo presegati {{limit}} znakov.', releaseNotes: 'Opombe o izdaji', }, action: { - updateFailure: 'Posodobitev različice ni uspela', - restoreFailure: 'Obnovitev različice ni uspela', - updateSuccess: 'Različica posodobljena', - restoreSuccess: 'Obnovljena različica', deleteSuccess: 'Različica izbrisana', deleteFailure: 'Brisanje različice ni uspelo', + updateFailure: 'Posodobitev različice ni uspela', + restoreSuccess: 'Obnovljena različica', + restoreFailure: 'Obnavljanje različice ni uspelo', + updateSuccess: 'Različica posodobljena', }, - releaseNotesPlaceholder: 'Opisujte, kaj se je spremenilo.', - latest: 'Najnovejši', - deletionTip: 'Izbris je nepovraten, prosim potrdite.', defaultName: 'Nepodpisana različica', - nameThisVersion: 'Poimenujte to različico', - restorationTip: 'Po obnovitvi različice bo trenutni osnutek prepisan.', + deletionTip: 'Izbris je nepovraten, prosim potrdite.', currentDraft: 'Trenutni osnutek', - editVersionInfo: 'Uredi informacije o različici', title: 'Različice', + editVersionInfo: 'Uredi informacije o različici', + latest: 'Najnovejši', + nameThisVersion: 'Poimenujte to različico', + releaseNotesPlaceholder: 'Opisujte, kaj se je spremenilo', + restorationTip: 'Po obnovitvi različice bo trenutni osnutek prepisan.', + }, + debug: { + noData: { + runThisNode: 'Zagon te vozlišča', + description: 'Rezultati zadnjega zagona bodo prikazani tukaj', + }, + variableInspect: { + trigger: { + stop: 'Ustavi se', + normal: 'Inspiciranje spremenljivk', + clear: 'Jasno', + cached: 'Poglej shranjene spremenljivke', + running: 'Shranjevanje statusa delovanja', + }, + emptyLink: 'Nauči se več', + chatNode: 'Pogovor', + envNode: 'Okolje', + systemNode: 'Sistem', + view: 'Oglej si dnevnik', + title: 'Inspiciranje spremenljivk', + clearNode: 'Počisti predpomnjeno spremenljivko', + clearAll: 'Ponastavi vse', + reset: 'Ponastavi na zadnjo vrednost izvajanja', + edited: 'Uredjeno', + resetConversationVar: 'Ponastavi spremenljivko pogovora na privzeto vrednost', + emptyTip: 'Po prehodu skozi vozlišče na platnu ali po zagonu vozlišča korak za korakom lahko v pregledu spremenljivk vidite trenutno vrednost spremenljivke vozlišča.', + }, + settingsTab: 'Nastavitve', + lastRunTab: 'Zadnji zagon', }, } diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 0979d07f51..9d2b9af398 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -93,6 +93,7 @@ const translation = { advancedUserDescription: 'โฟลว์พร้อมคุณสมบัติหน่วยความจำเพิ่มเติมและอินเตอร์เฟซแชทบอท', chooseAppType: 'เลือกประเภทแอป', advancedShortDescription: 'โฟลว์ที่เสริมประสิทธิภาพสำหรับการสนทนาหลายรอบ', + dropDSLToCreateApp: 'ลากไฟล์ DSL มาที่นี่เพื่สร้างแอป', }, editApp: 'แก้ไขข้อมูล', editAppTitle: 'แก้ไขข้อมูลโปรเจกต์', @@ -141,6 +142,14 @@ const translation = { notConfigured: 'ผู้ให้บริการกําหนดค่าเพื่อเปิดใช้งานการติดตาม', moreProvider: 'ผู้ให้บริการเพิ่มเติม', }, + arize: { + title: 'Arize', + description: 'การสังเกตการณ์ LLM ระดับองค์กร การประเมินออนไลน์และออฟไลน์ การตรวจสอบ และการทดลอง—ขับเคลื่อนโดย OpenTelemetry ออกแบบมาโดยเฉพาะสำหรับแอปพลิเคชันที่ขับเคลื่อนด้วย LLM และตัวแทน', + }, + phoenix: { + title: 'Phoenix', + description: 'แพลตฟอร์มโอเพ่นซอร์สและ OpenTelemetry สำหรับการสังเกตการณ์ การประเมิน วิศวกรรมพรอมต์ และการทดลองสำหรับเวิร์กโฟลว์และตัวแทน LLM ของคุณ', + }, langsmith: { title: 'Langsmith', description: 'แพลตฟอร์มนักพัฒนาแบบครบวงจรสําหรับทุกขั้นตอนของ การพัฒนาโปรเจกต์ที่ขับเคลื่อนด้วย LLM', @@ -168,6 +177,7 @@ const translation = { title: 'ทอ', description: 'Weave เป็นแพลตฟอร์มโอเพนซอร์สสำหรับการประเมินผล ทดสอบ และตรวจสอบแอปพลิเคชัน LLM', }, + aliyun: {}, }, mermaid: { handDrawn: 'วาดด้วยมือ', diff --git a/web/i18n/th-TH/common.ts b/web/i18n/th-TH/common.ts index 7425a178d3..353ede8052 100644 --- a/web/i18n/th-TH/common.ts +++ b/web/i18n/th-TH/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'ดาวน์โหลดล้มเหลว กรุณาลองอีกครั้งในภายหลัง.', more: 'มากขึ้น', downloadSuccess: 'ดาวน์โหลดเสร็จสิ้นแล้ว.', + selectAll: 'เลือกทั้งหมด', + deSelectAll: 'ยกเลิกการเลือกทั้งหมด', }, errorMsg: { fieldRequired: '{{field}} เป็นสิ่งจําเป็น', @@ -466,7 +468,6 @@ const translation = { apiBasedExtension: { title: 'ส่วนขยาย API ให้การจัดการ API แบบรวมศูนย์ ทําให้การกําหนดค่าง่ายขึ้นเพื่อให้ใช้งานได้ง่ายในแอปพลิเคชันของ Dify', link: 'เรียนรู้วิธีพัฒนาส่วนขยาย API ของคุณเอง', - linkUrl: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', add: 'เพิ่มส่วนขยาย API', selector: { title: 'ส่วนขยาย API', diff --git a/web/i18n/th-TH/dataset-creation.ts b/web/i18n/th-TH/dataset-creation.ts index e6081a9618..cd318984b1 100644 --- a/web/i18n/th-TH/dataset-creation.ts +++ b/web/i18n/th-TH/dataset-creation.ts @@ -71,7 +71,6 @@ const translation = { run: 'วิ่ง', firecrawlTitle: 'แยกเนื้อหาเว็บด้วย 🔥Firecrawl', firecrawlDoc: 'เอกสาร Firecrawl', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', jinaReaderTitle: 'แปลงทั้งไซต์เป็น Markdown', jinaReaderDoc: 'เรียนรู้เพิ่มเติมเกี่ยวกับ Jina Reader', jinaReaderDocLink: 'https://jina.ai/reader', @@ -94,7 +93,6 @@ const translation = { maxDepthTooltip: 'ความลึกสูงสุดในการรวบรวมข้อมูลเมื่อเทียบกับ URL ที่ป้อน ความลึก 0 เพียงแค่ขูดหน้าของ URL ที่ป้อนความลึก 1 ขูด url และทุกอย่างหลังจาก enteredURL + หนึ่ง / เป็นต้น', watercrawlTitle: 'ดึงเนื้อหาจากเว็บด้วย Watercrawl', configureJinaReader: 'ตั้งค่า Jina Reader', - watercrawlDocLink: 'https://docs.dify.ai/th/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureFirecrawl: 'กำหนดค่า Firecrawl', configureWatercrawl: 'กำหนดค่าการเข้าถึงน้ำ', waterCrawlNotConfiguredDescription: 'กำหนดค่า Watercrawl ด้วย API key เพื่อใช้งาน.', diff --git a/web/i18n/th-TH/dataset-documents.ts b/web/i18n/th-TH/dataset-documents.ts index 91d04d6bc1..966218ad93 100644 --- a/web/i18n/th-TH/dataset-documents.ts +++ b/web/i18n/th-TH/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'ลบ', enableWarning: 'ไม่สามารถเปิดใช้งานไฟล์ที่เก็บถาวรได้', sync: 'ซิงค์', + pause: 'หยุด', + resume: 'ดำเนิน', }, index: { enable: 'เปิด', @@ -388,6 +390,8 @@ const translation = { searchResults_other: 'ผลลัพธ์', regenerationSuccessMessage: 'คุณสามารถปิดหน้าต่างนี้ได้', childChunks_one: 'ก้อนเด็ก', + keywordDuplicate: 'คำสำคัญมีอยู่แล้ว', + keywordEmpty: 'คีย์เวิร์ดไม่สามารถว่างเปล่าได้', }, } diff --git a/web/i18n/th-TH/dataset.ts b/web/i18n/th-TH/dataset.ts index 15ef381605..ede1be1609 100644 --- a/web/i18n/th-TH/dataset.ts +++ b/web/i18n/th-TH/dataset.ts @@ -178,6 +178,7 @@ const translation = { checkName: { invalid: 'ชื่อเมตาดาต้าต้องประกอบด้วยตัวอักษรตัวเล็กเท่านั้น เลข และขีดล่าง และต้องเริ่มต้นด้วยตัวอักษรตัวเล็ก', empty: 'ชื่อข้อมูลเมตาไม่สามารถเป็นค่าแEmpty', + tooLong: 'ชื่อเมตาดาต้าไม่สามารถเกิน {{max}} ตัวอักษร', }, batchEditMetadata: { multipleValue: 'หลายค่า', diff --git a/web/i18n/th-TH/plugin.ts b/web/i18n/th-TH/plugin.ts index de7ed8c84e..92684a4be8 100644 --- a/web/i18n/th-TH/plugin.ts +++ b/web/i18n/th-TH/plugin.ts @@ -137,6 +137,7 @@ const translation = { installedSuccessfully: 'การติดตั้งสําเร็จ', installComplete: 'การติดตั้งเสร็จสมบูรณ์', pluginLoadError: 'ข้อผิดพลาดในการโหลดปลั๊กอิน', + installWarning: 'ไม่อนุญาตให้ติดตั้งปลั๊กอินนี้', }, installFromGitHub: { updatePlugin: 'อัปเดตปลั๊กอินจาก GitHub', @@ -205,13 +206,13 @@ const translation = { searchTools: 'เครื่องมือค้นหา...', installFrom: 'ติดตั้งจาก', fromMarketplace: 'จาก Marketplace', - submitPlugin: 'ส่งปลั๊กอิน', allCategories: 'หมวดหมู่ทั้งหมด', metadata: { title: 'ปลั๊กอิน', }, difyVersionNotCompatible: 'เวอร์ชั่นปัจจุบันของ Dify ไม่สามารถใช้งานร่วมกับปลั๊กอินนี้ได้ กรุณาอัปเกรดไปยังเวอร์ชั่นขั้นต่ำที่ต้องการ: {{minimalDifyVersion}}', requestAPlugin: 'ขอปลั๊กอิน', + publishPlugins: 'เผยแพร่ปลั๊กอิน', } export default translation diff --git a/web/i18n/th-TH/tools.ts b/web/i18n/th-TH/tools.ts index 14c9457c4e..df36463e57 100644 --- a/web/i18n/th-TH/tools.ts +++ b/web/i18n/th-TH/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'เพิ่ม', added: 'เพิ่ม', manageInTools: 'จัดการในเครื่องมือ', - emptyTitle: 'ไม่มีเครื่องมือเวิร์กโฟลว์', - emptyTip: 'ไปที่ "เวิร์กโฟลว์ -> เผยแพร่เป็นเครื่องมือ"', - emptyTitleCustom: 'ไม่มีเครื่องมือที่กําหนดเอง', - emptyTipCustom: 'สร้างเครื่องมือแบบกําหนดเอง', + custom: { + title: 'ไม่มีเครื่องมือกำหนดเอง', + tip: 'สร้างเครื่องมือกำหนดเอง', + }, + workflow: { + title: 'ไม่มีเครื่องมือเวิร์กโฟลว์', + tip: 'เผยแพร่เวิร์กโฟลว์เป็นเครื่องมือใน Studio', + }, + mcp: { + title: 'ไม่มีเครื่องมือ MCP', + tip: 'เพิ่มเซิร์ฟเวอร์ MCP', + }, + agent: { + title: 'ไม่มีกลยุทธ์เอเจนต์', + }, }, createTool: { title: 'สร้างเครื่องมือที่กําหนดเอง', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'ชื่อการเรียกเครื่องมือสําหรับการใช้เหตุผลและการแจ้งเตือนของตัวแทน', noTools: 'ไม่พบเครื่องมือ', copyToolName: 'คัดลอกชื่อ', + mcp: { + create: { + cardTitle: 'เพิ่มเซิร์ฟเวอร์ MCP (HTTP)', + cardLink: 'เรียนรู้เพิ่มเติมเกี่ยวกับการรวมเซิร์ฟเวอร์ MCP', + }, + noConfigured: 'เซิร์ฟเวอร์ที่ยังไม่ได้กำหนดค่า', + updateTime: 'อัปเดตแล้ว', + toolsCount: '{count} เครื่องมือ', + noTools: 'ไม่มีเครื่องมือที่ใช้ได้', + modal: { + title: 'เพิ่มเซิร์ฟเวอร์ MCP (HTTP)', + editTitle: 'แก้ไขเซิร์ฟเวอร์ MCP (HTTP)', + name: 'ชื่อ & ไอคอน', + namePlaceholder: 'ตั้งชื่อเซิร์ฟเวอร์ MCP ของคุณ', + serverUrl: 'URL ของเซิร์ฟเวอร์', + serverUrlPlaceholder: 'URL สำหรับจุดสิ้นสุดของเซิร์ฟเวอร์', + serverUrlWarning: 'การอัปเดตที่อยู่เซิร์ฟเวอร์อาจทำให้แอปพลิเคชันที่พึ่งพาเซิร์ฟเวอร์นี้หยุดทำงาน', + serverIdentifier: 'ตัวระบุเซิร์ฟเวอร์', + serverIdentifierTip: 'ตัวระบุที่ไม่ซ้ำกันสำหรับเซิร์ฟเวอร์ MCP ภายในพื้นที่ทำงาน ตัวอักษรเล็ก ตัวเลข ขีดล่าง และขีดกลางเท่านั้น ความยาวไม่เกิน 24 ตัวอักษร', + serverIdentifierPlaceholder: 'ตัวระบุที่ไม่ซ้ำกัน เช่น my-mcp-server', + serverIdentifierWarning: 'เซิร์ฟเวอร์จะไม่ถูกต้องในแอปพลิเคชันที่มีอยู่หลังจากการเปลี่ยน ID', + cancel: 'ยกเลิก', + save: 'บันทึก', + confirm: 'เพิ่มและอนุญาต', + }, + delete: 'ลบเซิร์ฟเวอร์ MCP', + deleteConfirmTitle: 'คุณต้องการลบ {mcp} หรือไม่?', + operation: { + edit: 'แก้ไข', + remove: 'ลบ', + }, + authorize: 'อนุญาต', + authorizing: 'กำลังอนุญาต...', + authorizingRequired: 'ต้องมีการอนุญาต', + authorizeTip: 'หลังจากอนุญาต เครื่องมือจะถูกแสดงที่นี่', + update: 'อัปเดต', + updating: 'กำลังอัปเดต', + gettingTools: 'กำลังโหลดเครื่องมือ...', + updateTools: 'กำลังอัปเดตเครื่องมือ...', + toolsEmpty: 'ยังไม่โหลดเครื่องมือ', + getTools: 'รับเครื่องมือ', + toolUpdateConfirmTitle: 'อัปเดตรายการเครื่องมือ', + toolUpdateConfirmContent: 'การอัปเดตรายการเครื่องมืออาจส่งผลต่อแอปพลิเคชันที่มีอยู่ คุณต้องการดำเนินการต่อหรือไม่?', + toolsNum: '{count} เครื่องมือที่รวมอยู่', + onlyTool: 'รวม 1 เครื่องมือ', + identifier: 'ตัวระบุเซิร์ฟเวอร์ (คลิกเพื่อคัดลอก)', + server: { + title: 'เซิร์ฟเวอร์ MCP', + url: 'URL ของเซิร์ฟเวอร์', + reGen: 'คุณต้องการสร้าง URL ของเซิร์ฟเวอร์ใหม่หรือไม่?', + addDescription: 'เพิ่มคำอธิบาย', + edit: 'แก้ไขคำอธิบาย', + modal: { + addTitle: 'เพิ่มคำอธิบายเพื่อเปิดใช้งานเซิร์ฟเวอร์ MCP', + editTitle: 'แก้ไขคำอธิบาย', + description: 'คำอธิบาย', + descriptionPlaceholder: 'อธิบายว่าเครื่องมือนี้ทำอะไรและควรใช้กับ LLM อย่างไร', + parameters: 'พารามิเตอร์', + parametersTip: 'เพิ่มคำอธิบายสำหรับแต่ละพารามิเตอร์เพื่อช่วยให้ LLM เข้าใจวัตถุประสงค์และข้อจำกัดของมัน', + parametersPlaceholder: 'วัตถุประสงค์และข้อจำกัดของพารามิเตอร์', + confirm: 'เปิดใช้งานเซิร์ฟเวอร์ MCP', + }, + publishTip: 'แอปไม่ถูกเผยแพร่ กรุณาเผยแพร่แอปก่อน', + }, + }, } export default translation diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index cc2cee418f..28be5c57e8 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -42,8 +42,6 @@ const translation = { setVarValuePlaceholder: 'ตั้งค่าตัวแปร', needConnectTip: 'ขั้นตอนนี้ไม่ได้เชื่อมต่อกับสิ่งใด', maxTreeDepth: 'ขีดจํากัดสูงสุดของ {{depth}} โหนดต่อสาขา', - needEndNode: 'ต้องเพิ่มบล็อก End', - needAnswerNode: 'ต้องเพิ่มบล็อกคําตอบ', workflowProcess: 'กระบวนการเวิร์กโฟลว์', notRunning: 'ยังไม่ได้ทํางาน', previewPlaceholder: 'ป้อนเนื้อหาในช่องด้านล่างเพื่อเริ่มแก้ไขข้อบกพร่องของแชทบอท', @@ -62,7 +60,6 @@ const translation = { learnMore: 'ศึกษาเพิ่มเติม', copy: 'ลอก', duplicate: 'สำเนา', - addBlock: 'เพิ่มบล็อก', pasteHere: 'วางที่นี่', pointerMode: 'โหมดตัวชี้', handMode: 'โหมดมือ', @@ -115,6 +112,9 @@ const translation = { exitVersions: 'ออกเวอร์ชัน', exportImage: 'ส่งออกภาพ', exportSVG: 'ส่งออกเป็น SVG', + needAnswerNode: 'ต้องเพิ่มโหนดคำตอบ', + addBlock: 'เพิ่มโนด', + needEndNode: 'ต้องเพิ่มโหนดจบ', }, env: { envPanelTitle: 'ตัวแปรสภาพแวดล้อม', @@ -129,6 +129,8 @@ const translation = { value: 'ค่า', valuePlaceholder: 'ค่า env', secretTip: 'ใช้เพื่อกําหนดข้อมูลหรือข้อมูลที่ละเอียดอ่อน โดยมีการตั้งค่า DSL ที่กําหนดค่าไว้เพื่อป้องกันการรั่วไหล', + description: 'คำอธิบาย', + descriptionPlaceholder: 'อธิบายตัวแปร', }, export: { title: 'ส่งออกตัวแปรสภาพแวดล้อม Secret หรือไม่', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} ก้าวไปข้างหน้า', sessionStart: 'เริ่มเซสชัน', currentState: 'สถานะปัจจุบัน', - nodeTitleChange: 'เปลี่ยนชื่อบล็อก', - nodeDescriptionChange: 'คําอธิบายบล็อกเปลี่ยนไป', - nodeDragStop: 'บล็อกย้าย', - nodeChange: 'บล็อกเปลี่ยนไป', - nodeConnect: 'บล็อกเชื่อมต่อ', - nodePaste: 'บล็อกวาง', - nodeDelete: 'บล็อกลบ', - nodeAdd: 'เพิ่มบล็อก', - nodeResize: 'บล็อกปรับขนาด', noteAdd: 'เพิ่มหมายเหตุ', noteChange: 'เปลี่ยนหมายเหตุ', noteDelete: 'ลบโน้ต', - edgeDelete: 'บล็อกตัดการเชื่อมต่อ', + nodeDelete: 'โหนดถูกลบแล้ว', + nodeDescriptionChange: 'คำอธิบายของโหนดถูกเปลี่ยน', + nodeDragStop: 'โหนดถูกย้าย', + edgeDelete: 'เชื่อมต่อ Node ขาดหาย', + nodeTitleChange: 'ชื่อโหนดเปลี่ยน', + nodeAdd: 'เพิ่มโนด', + nodeChange: 'โหนดเปลี่ยนแปลง', + nodeResize: 'ขนาดของโหนดถูกปรับขนาด', + nodeConnect: 'เชื่อมต่อ Node', + nodePaste: 'โนดที่วางไว้', }, errorMsg: { fieldRequired: '{{field}} เป็นสิ่งจําเป็น', @@ -217,8 +219,6 @@ const translation = { loop: 'ลูป', }, tabs: { - 'searchBlock': 'บล็อกการค้นหา', - 'blocks': 'บล็อก', 'searchTool': 'เครื่องมือค้นหา', 'tools': 'เครื่อง มือ', 'allTool': 'ทั้งหมด', @@ -232,6 +232,8 @@ const translation = { 'noResult': 'ไม่พบการจับคู่', 'agent': 'กลยุทธ์ตัวแทน', 'plugin': 'ปลั๊กอิน', + 'searchBlock': 'ค้นหาโหนด', + 'blocks': 'โหนด', }, blocks: { 'start': 'เริ่ม', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้', - changeBlock: 'เปลี่ยนบล็อก', helpLink: 'ลิงค์ช่วยเหลือ', about: 'ประมาณ', createdBy: 'สร้างโดย', nextStep: 'ขั้นตอนถัดไป', - addNextStep: 'เพิ่มบล็อกถัดไปในเวิร์กโฟลว์นี้', - selectNextStep: 'เลือกบล็อกถัดไป', runThisStep: 'เรียกใช้ขั้นตอนนี้', checklist: 'ตรวจ สอบ', checklistTip: 'ตรวจสอบให้แน่ใจว่าปัญหาทั้งหมดได้รับการแก้ไขแล้วก่อนที่จะเผยแพร่', checklistResolved: 'ปัญหาทั้งหมดได้รับการแก้ไขแล้ว', - organizeBlocks: 'จัดระเบียบบล็อก', change: 'เปลี่ยน', optional: '(ไม่บังคับ)', moveToThisNode: 'ย้ายไปที่โหนดนี้', + organizeBlocks: 'จัดระเบียบโหนด', + addNextStep: 'เพิ่มขั้นตอนถัดไปในกระบวนการทำงานนี้', + changeBlock: 'เปลี่ยนโหนด', + selectNextStep: 'เลือกขั้นตอนถัดไป', + minimize: 'ออกจากโหมดเต็มหน้าจอ', + maximize: 'เพิ่มประสิทธิภาพผ้าใบ', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retries: '{{num}} ลอง', ms: 'นางสาว', }, + typeSwitch: {}, }, start: { required: 'ต้องระบุ', @@ -535,6 +540,10 @@ const translation = { title: 'นําเข้าจาก cURL', placeholder: 'วางสตริง cURL ที่นี่', }, + verifySSL: { + title: 'ตรวจสอบใบรับรอง SSL', + warningTooltip: 'การปิดการตรวจสอบ SSL ไม่แนะนำให้ใช้ในสภาพแวดล้อมการผลิต ควรใช้เฉพาะในระหว่างการพัฒนาหรือการทดสอบเท่านั้น เนื่องจากจะทำให้การเชื่อมต่อมีความเสี่ยงต่อภัยคุกคามด้านความปลอดภัย เช่น การโจมตีแบบ Man-in-the-middle.', + }, }, code: { inputVars: 'ตัวแปรอินพุต', @@ -666,6 +675,7 @@ const translation = { inputVars: 'ตัวแปรอินพุต', outputVars: { className: 'ชื่อคลาส', + usage: 'ข้อมูลการใช้งานรุ่น', }, class: 'ประเภท', classNamePlaceholder: 'เขียนชื่อชั้นเรียนของคุณ', @@ -679,6 +689,11 @@ const translation = { }, parameterExtractor: { inputVar: 'ตัวแปรอินพุต', + outputVars: { + isSuccess: 'คือ Success เมื่อสําเร็จค่าคือ 1 เมื่อล้มเหลวค่าเป็น 0', + errorReason: 'สาเหตุข้อผิดพลาด', + usage: 'ข้อมูลการใช้งานรุ่น', + }, extractParameters: 'แยกพารามิเตอร์', importFromTool: 'นําเข้าจากเครื่องมือ', addExtractParameter: 'เพิ่มพารามิเตอร์การแยกข้อมูล', @@ -698,8 +713,6 @@ const translation = { advancedSetting: 'การตั้งค่าขั้นสูง', reasoningMode: 'โหมดการให้เหตุผล', reasoningModeTip: 'คุณสามารถเลือกโหมดการให้เหตุผลที่เหมาะสมตามความสามารถของโมเดลในการตอบสนองต่อคําแนะนําสําหรับการเรียกใช้ฟังก์ชันหรือข้อความแจ้ง', - isSuccess: 'คือ Success เมื่อสําเร็จค่าคือ 1 เมื่อล้มเหลวค่าเป็น 0', - errorReason: 'สาเหตุข้อผิดพลาด', }, iteration: { deleteTitle: 'ลบโหนดการทําซ้ํา?', @@ -916,6 +929,35 @@ const translation = { title: 'เวอร์ชัน', latest: 'ล่าสุด', }, + debug: { + noData: { + runThisNode: 'ทำงานโหนดนี้', + description: 'ผลลัพธ์จากการวิ่งครั้งล่าสุดจะแสดงที่นี่', + }, + variableInspect: { + trigger: { + stop: 'หยุดวิ่ง', + normal: 'ตรวจสอบตัวแปร', + cached: 'ดูตัวแปรที่ถูกเก็บไว้ในแคช', + clear: 'ชัดเจน', + running: 'สถานะการทำงานของการเก็บข้อมูลชั่วคราว', + }, + systemNode: 'ระบบ', + view: 'ดูบันทึก', + chatNode: 'การสนทนา', + clearAll: 'รีเซ็ตทั้งหมด', + envNode: 'สิ่งแวดล้อม', + emptyLink: 'เรียนรู้เพิ่มเติม', + edited: 'แก้ไขแล้ว', + reset: 'รีเซ็ตกลับไปยังค่าครั้งล่าสุด', + title: 'ตรวจสอบตัวแปร', + resetConversationVar: 'รีเซ็ตตัวแปรการสนทนาไปยังค่าตั้งต้น', + emptyTip: 'หลังจากก้าวผ่านโหนดบนผืนผ้าใบหรือเรียกใช้โหนดทีละขั้นตอน คุณสามารถดูค่าปัจจุบันของตัวแปรโหนดใน Variable Inspect ได้', + clearNode: 'ล้างตัวแปรที่เก็บไว้ในแคช', + }, + settingsTab: 'การตั้งค่า', + lastRunTab: 'รอบสุดท้าย', + }, } export default translation diff --git a/web/i18n/tr-TR/app-debug.ts b/web/i18n/tr-TR/app-debug.ts index f08d221d45..6ed0e0a5eb 100644 --- a/web/i18n/tr-TR/app-debug.ts +++ b/web/i18n/tr-TR/app-debug.ts @@ -368,6 +368,7 @@ const translation = { writeOpener: 'Başlangıç mesajı yaz', placeholder: 'Başlangıç mesajınızı buraya yazın, değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin.', openingQuestion: 'Açılış Soruları', + openingQuestionPlaceholder: 'Değişkenler kullanabilirsiniz, {{variable}} yazmayı deneyin.', noDataPlaceHolder: 'Kullanıcı ile konuşmayı başlatmak, AI\'ın konuşma uygulamalarında onlarla daha yakın bir bağlantı kurmasına yardımcı olabilir.', varTip: 'Değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 5e55ffa349..16bad22231 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -93,6 +93,7 @@ const translation = { advancedShortDescription: 'Çok turlu sohbetler için geliştirilmiş iş akışı', noIdeaTip: 'Fikriniz yok mu? Şablonlarımıza göz atın', forAdvanced: 'İLERI DÜZEY KULLANICILAR IÇIN', + dropDSLToCreateApp: 'Uygulama oluşturmak için DSL dosyasını buraya bırakın', }, editApp: 'Bilgileri Düzenle', editAppTitle: 'Uygulama Bilgilerini Düzenle', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'İzlemeyi etkinleştirmek için sağlayıcıyı yapılandırın', moreProvider: 'Daha Fazla Sağlayıcı', }, + arize: { + title: 'Arize', + description: 'Kurumsal düzeyde LLM gözlemlenebilirliği, çevrimiçi ve çevrimdışı değerlendirme, izleme ve deneyler — OpenTelemetry ile desteklenmektedir. LLM ve ajan tabanlı uygulamalar için özel olarak tasarlanmıştır.', + }, + phoenix: { + title: 'Phoenix', + description: 'LLM iş akışlarınız ve ajanlarınız için açık kaynaklı ve OpenTelemetry tabanlı gözlemlenebilirlik, değerlendirme, istem mühendisliği ve deney platformu.', + }, langsmith: { title: 'LangSmith', description: 'LLM destekli uygulama yaşam döngüsünün her adımı için her şeyi kapsayan bir geliştirici platformu.', @@ -163,6 +172,7 @@ const translation = { title: 'Dokuma', description: 'Weave, LLM uygulamalarını değerlendirmek, test etmek ve izlemek için açık kaynaklı bir platformdur.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 web app simgesinin kullanılıp kullanılmayacağı', diff --git a/web/i18n/tr-TR/common.ts b/web/i18n/tr-TR/common.ts index 62ce150986..9ecbba3de5 100644 --- a/web/i18n/tr-TR/common.ts +++ b/web/i18n/tr-TR/common.ts @@ -58,6 +58,8 @@ const translation = { format: 'Format', more: 'Daha fazla', downloadFailed: 'İndirme başarısız oldu. Lütfen daha sonra tekrar deneyin.', + selectAll: 'Hepsini Seç', + deSelectAll: 'Hepsini Seçme', }, errorMsg: { fieldRequired: '{{field}} gereklidir', @@ -471,7 +473,6 @@ const translation = { apiBasedExtension: { title: 'API uzantıları merkezi API yönetimi sağlar, Dify\'nin uygulamaları arasında kolay kullanım için yapılandırmayı basitleştirir.', link: 'Kendi API Uzantınızı nasıl geliştireceğinizi öğrenin.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'API Uzantısı Ekle', selector: { title: 'API Uzantısı', diff --git a/web/i18n/tr-TR/dataset-creation.ts b/web/i18n/tr-TR/dataset-creation.ts index cb3cfcfac4..41dc49b7ca 100644 --- a/web/i18n/tr-TR/dataset-creation.ts +++ b/web/i18n/tr-TR/dataset-creation.ts @@ -63,7 +63,6 @@ const translation = { run: 'Çalıştır', firecrawlTitle: '🔥Firecrawl ile web içeriğini çıkarın', firecrawlDoc: 'Firecrawl dokümanları', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', options: 'Seçenekler', crawlSubPage: 'Alt sayfaları tarayın', limit: 'Sınır', @@ -93,7 +92,6 @@ const translation = { waterCrawlNotConfigured: 'Watercrawl yapılandırılmamış', watercrawlTitle: 'Watercrawl ile web içeriğini çıkar', configureJinaReader: 'Jina Okuyucusunu Yapılandır', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', configureWatercrawl: 'Watercrawl\'ı yapılandır', }, cancel: 'İptal', diff --git a/web/i18n/tr-TR/dataset-documents.ts b/web/i18n/tr-TR/dataset-documents.ts index f643375334..e7118b8c9b 100644 --- a/web/i18n/tr-TR/dataset-documents.ts +++ b/web/i18n/tr-TR/dataset-documents.ts @@ -29,6 +29,8 @@ const translation = { delete: 'Sil', enableWarning: 'Arşivlenmiş dosya etkinleştirilemez', sync: 'Senkronize et', + pause: 'Duraklat', + resume: 'Devam Et', }, index: { enable: 'Etkinleştir', @@ -388,6 +390,8 @@ const translation = { chunks_other: 'Parçalar', editedAt: 'Şurada düzenlendi:', addChildChunk: 'Alt Parça Ekle', + keywordDuplicate: 'Anahtar kelime zaten var', + keywordEmpty: 'Anahtar kelime boş olamaz', }, } diff --git a/web/i18n/tr-TR/dataset.ts b/web/i18n/tr-TR/dataset.ts index 96f120c13d..1875d5bd40 100644 --- a/web/i18n/tr-TR/dataset.ts +++ b/web/i18n/tr-TR/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: 'Meta veri adı boş olamaz', invalid: 'Meta verisi adı yalnızca küçük harfler, sayılar ve alt çizgiler içerebilir ve küçük bir harfle başlamalıdır.', + tooLong: 'Meta veri adı {{max}} karakteri geçemez', }, batchEditMetadata: { multipleValue: 'Birden Fazla Değer', diff --git a/web/i18n/tr-TR/plugin.ts b/web/i18n/tr-TR/plugin.ts index aef546589c..a2d34584fc 100644 --- a/web/i18n/tr-TR/plugin.ts +++ b/web/i18n/tr-TR/plugin.ts @@ -137,6 +137,7 @@ const translation = { readyToInstallPackages: 'Aşağıdaki {{num}} eklentilerini yüklemek üzereyim', dropPluginToInstall: 'Yüklemek için eklenti paketini buraya bırakın', installPlugin: 'Eklentiyi Yükle', + installWarning: 'Bu eklentinin yüklenmesine izin verilmemektedir.', }, installFromGitHub: { installedSuccessfully: 'Yükleme başarılı', @@ -197,7 +198,6 @@ const translation = { search: 'Aramak', install: '{{num}} yükleme', searchPlugins: 'Eklentileri ara', - submitPlugin: 'Eklenti gönder', searchTools: 'Arama araçları...', fromMarketplace: 'Pazar Yerinden', installPlugin: 'Eklentiyi yükle', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Mevcut Dify sürümü bu eklentiyle uyumlu değil, lütfen gerekli minimum sürüme güncelleyin: {{minimalDifyVersion}}', requestAPlugin: 'Bir eklenti iste', + publishPlugins: 'Eklentileri yayınlayın', } export default translation diff --git a/web/i18n/tr-TR/tools.ts b/web/i18n/tr-TR/tools.ts index af9ddf182f..6e641165e2 100644 --- a/web/i18n/tr-TR/tools.ts +++ b/web/i18n/tr-TR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'Ekle', added: 'Eklendi', manageInTools: 'Araçlarda Yönet', - emptyTitle: 'Kullanılabilir workflow aracı yok', - emptyTip: 'Git "Workflow -> Araç olarak Yayınla"', - emptyTitleCustom: 'Özel bir araç yok', - emptyTipCustom: 'Özel bir araç oluşturun', + custom: { + title: 'Mevcut özel araç yok', + tip: 'Özel bir araç oluşturun', + }, + workflow: { + title: 'Mevcut iş akışı aracı yok', + tip: 'İş akışlarını Studio\'da araç olarak yayınlayın', + }, + mcp: { + title: 'Mevcut MCP aracı yok', + tip: 'Bir MCP sunucusu ekleyin', + }, + agent: { + title: 'Mevcut ajan stratejisi yok', + }, }, createTool: { title: 'Özel Araç Oluştur', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Agent akıl yürütme ve prompt için araç çağrı adı', copyToolName: 'Adı Kopyala', noTools: 'Araç bulunamadı', + mcp: { + create: { + cardTitle: 'MCP Sunucusu Ekle (HTTP)', + cardLink: 'MCP sunucu entegrasyonu hakkında daha fazla bilgi edinin', + }, + noConfigured: 'Yapılandırılmamış Sunucu', + updateTime: 'Güncellendi', + toolsCount: '{count} araç', + noTools: 'Kullanılabilir araç yok', + modal: { + title: 'MCP Sunucusu Ekle (HTTP)', + editTitle: 'MCP Sunucusunu Düzenle (HTTP)', + name: 'Ad ve Simge', + namePlaceholder: 'MCP sunucunuza ad verin', + serverUrl: 'Sunucu URL', + serverUrlPlaceholder: 'Sunucu endpoint URL', + serverUrlWarning: 'Sunucu adresini güncellemek, bu sunucuya bağımlı uygulamaları kesintiye uğratabilir', + serverIdentifier: 'Sunucu Tanımlayıcı', + serverIdentifierTip: 'Çalışma alanındaki MCP sunucusu için benzersiz tanımlayıcı. Sadece küçük harf, rakam, alt çizgi ve tire. En fazla 24 karakter.', + serverIdentifierPlaceholder: 'Benzersiz tanımlayıcı, örn. my-mcp-server', + serverIdentifierWarning: 'ID değiştirildikten sonra sunucu mevcut uygulamalar tarafından tanınmayacak', + cancel: 'İptal', + save: 'Kaydet', + confirm: 'Ekle ve Yetkilendir', + }, + delete: 'MCP Sunucusunu Kaldır', + deleteConfirmTitle: '{mcp} kaldırılsın mı?', + operation: { + edit: 'Düzenle', + remove: 'Kaldır', + }, + authorize: 'Yetkilendir', + authorizing: 'Yetkilendiriliyor...', + authorizingRequired: 'Yetkilendirme gerekli', + authorizeTip: 'Yetkilendirmeden sonra araçlar burada görüntülenecektir.', + update: 'Güncelle', + updating: 'Güncelleniyor...', + gettingTools: 'Araçlar alınıyor...', + updateTools: 'Araçlar güncelleniyor...', + toolsEmpty: 'Araçlar yüklenmedi', + getTools: 'Araçları al', + toolUpdateConfirmTitle: 'Araç Listesini Güncelle', + toolUpdateConfirmContent: 'Araç listesini güncellemek mevcut uygulamaları etkileyebilir. Devam etmek istiyor musunuz?', + toolsNum: '{count} araç dahil', + onlyTool: '1 araç dahil', + identifier: 'Sunucu Tanımlayıcı (Kopyalamak için Tıklayın)', + server: { + title: 'MCP Sunucusu', + url: 'Sunucu URL', + reGen: 'Sunucu URL yeniden oluşturulsun mu?', + addDescription: 'Açıklama ekle', + edit: 'Açıklamayı düzenle', + modal: { + addTitle: 'MCP Sunucusunu etkinleştirmek için açıklama ekleyin', + editTitle: 'Açıklamayı düzenle', + description: 'Açıklama', + descriptionPlaceholder: 'Bu aracın ne yaptığını ve LLM tarafından nasıl kullanılması gerektiğini açıklayın', + parameters: 'Parametreler', + parametersTip: 'LLM\'nin amaçlarını ve kısıtlamalarını anlamasına yardımcı olmak için her parametreye açıklamalar ekleyin.', + parametersPlaceholder: 'Parametre amacı ve kısıtlamaları', + confirm: 'MCP Sunucusunu Etkinleştir', + }, + publishTip: 'Uygulama yayınlanmadı. Lütfen önce uygulamayı yayınlayın.', + }, + }, } export default translation diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index 1358bbf645..a09e5d9068 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Değişkeni ayarla', needConnectTip: 'Bu adım hiçbir şeye bağlı değil', maxTreeDepth: 'Her dal için maksimum {{depth}} düğüm limiti', - needEndNode: 'Son blok eklenmelidir', - needAnswerNode: 'Yanıt bloğu eklenmelidir', workflowProcess: 'Workflow Süreci', notRunning: 'Henüz çalıştırılmadı', previewPlaceholder: 'Sohbet Robotunu hata ayıklamak için aşağıdaki kutuya içerik girin', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Daha Fazla Bilgi', copy: 'Kopyala', duplicate: 'Çoğalt', - addBlock: 'Blok Ekle', pasteHere: 'Buraya Yapıştır', pointerMode: 'İşaretçi Modu', handMode: 'El Modu', @@ -115,6 +112,9 @@ const translation = { noExist: 'Böyle bir değişken yok', exportSVG: 'SVG olarak dışa aktar', referenceVar: 'Referans Değişken', + addBlock: 'Düğüm Ekle', + needAnswerNode: 'Cevap düğümü eklenmelidir.', + needEndNode: 'Son düğüm eklenmelidir', }, env: { envPanelTitle: 'Çevre Değişkenleri', @@ -129,6 +129,8 @@ const translation = { value: 'Değer', valuePlaceholder: 'env değeri', secretTip: 'Hassas bilgileri veya verileri tanımlamak için kullanılır, bilgi sızıntısını önlemek için DSL ayarları yapılandırılmıştır.', + description: 'Açıklama', + descriptionPlaceholder: 'Değişkeni açıklayın', }, export: { title: 'Gizli çevre değişkenleri dışa aktarılsın mı?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} adım ileri', sessionStart: 'Oturum Başladı', currentState: 'Geçerli Durum', - nodeTitleChange: 'Blok başlığı değiştirildi', - nodeDescriptionChange: 'Blok açıklaması değiştirildi', - nodeDragStop: 'Blok taşındı', - nodeChange: 'Blok değiştirildi', - nodeConnect: 'Blok bağlandı', - nodePaste: 'Blok yapıştırıldı', - nodeDelete: 'Blok silindi', - nodeAdd: 'Blok eklendi', - nodeResize: 'Blok yeniden boyutlandırıldı', noteAdd: 'Not eklendi', noteChange: 'Not değiştirildi', noteDelete: 'Not silindi', - edgeDelete: 'Blok bağlantısı kesildi', + nodeDragStop: 'Düğüm taşındı', + nodeConnect: 'Node bağlandı', + nodeDescriptionChange: 'Düğüm açıklaması değiştirildi', + edgeDelete: 'Düğüm bağlantısı kesildi', + nodeChange: 'Düğüm değişti', + nodeDelete: 'Düğüm silindi', + nodeResize: 'Düğüm boyutu değiştirildi', + nodeTitleChange: 'Düğüm başlığı değiştirildi', + nodeAdd: 'Düğüm eklendi', + nodePaste: 'Düğüm yapıştırıldı', }, errorMsg: { fieldRequired: '{{field}} gereklidir', @@ -217,8 +219,6 @@ const translation = { loop: 'Döngü', }, tabs: { - 'searchBlock': 'Blok ara', - 'blocks': 'Bloklar', 'tools': 'Araçlar', 'allTool': 'Hepsi', 'builtInTool': 'Yerleşik', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Arama aracı', 'agent': 'Temsilci Stratejisi', 'plugin': 'Eklenti', + 'blocks': 'Düğümler', + 'searchBlock': 'Arama düğümü', }, blocks: { 'start': 'Başlat', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Kullanıcı Giriş Alanı', - changeBlock: 'Blok Değiştir', helpLink: 'Yardım Linki', about: 'Hakkında', createdBy: 'Oluşturan: ', nextStep: 'Sonraki Adım', - addNextStep: 'Bu iş akışında sonraki bloğu ekleyin', - selectNextStep: 'Sonraki Bloğu Seç', runThisStep: 'Bu adımı çalıştır', checklist: 'Kontrol Listesi', checklistTip: 'Yayınlamadan önce tüm sorunların çözüldüğünden emin olun', checklistResolved: 'Tüm sorunlar çözüldü', - organizeBlocks: 'Blokları Düzenle', change: 'Değiştir', optional: '(isteğe bağlı)', moveToThisNode: 'Bu düğüme geç', + changeBlock: 'Düğümü Değiştir', + addNextStep: 'Bu iş akışına bir sonraki adımı ekleyin', + organizeBlocks: 'Düğümleri düzenle', + selectNextStep: 'Sonraki Adımı Seç', + minimize: 'Tam Ekrandan Çık', + maximize: 'Kanvası Maksimize Et', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retrying: 'Yeniden deneniyor...', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'gerekli', @@ -536,6 +541,10 @@ const translation = { placeholder: 'cURL dizesini buraya yapıştırın', title: 'cURL\'den içe aktar', }, + verifySSL: { + title: 'SSL Sertifikasını Doğrula', + warningTooltip: 'SSL doğrulamasını devre dışı bırakmak, üretim ortamları için önerilmez. Bu yalnızca geliştirme veya test aşamalarında kullanılmalıdır, çünkü bağlantıyı adam ortada saldırıları gibi güvenlik tehditlerine karşı savunmasız hale getirir.', + }, }, code: { inputVars: 'Giriş Değişkenleri', @@ -668,6 +677,7 @@ const translation = { inputVars: 'Giriş Değişkenleri', outputVars: { className: 'Sınıf Adı', + usage: 'Model Kullanım Bilgileri', }, class: 'Sınıf', classNamePlaceholder: 'Sınıf adınızı yazın', @@ -681,6 +691,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Giriş Değişkeni', + outputVars: { + isSuccess: 'Başarılı mı. Başarılı olduğunda değer 1, başarısız olduğunda değer 0\'dır.', + errorReason: 'Hata Nedeni', + usage: 'Model Kullanım Bilgileri', + }, extractParameters: 'Parametreleri Çıkar', importFromTool: 'Araçlardan içe aktar', addExtractParameter: 'Çıkarma Parametresi Ekle', @@ -700,8 +715,6 @@ const translation = { advancedSetting: 'Gelişmiş Ayarlar', reasoningMode: 'Akıl Yürütme Modu', reasoningModeTip: 'Modelin fonksiyon çağırma veya istemler için talimatlara yanıt verme yeteneğine bağlı olarak uygun akıl yürütme modunu seçebilirsiniz.', - isSuccess: 'Başarılı mı. Başarılı olduğunda değer 1, başarısız olduğunda değer 0\'dır.', - errorReason: 'Hata Nedeni', }, iteration: { deleteTitle: 'Yineleme Düğümünü Sil?', @@ -918,6 +931,35 @@ const translation = { nameThisVersion: 'Bu versiyona isim ver', deletionTip: 'Silme işlemi geri alınamaz, lütfen onaylayın.', }, + debug: { + noData: { + runThisNode: 'Bu düğümü çalıştır', + description: 'Son çalışmanın sonuçları burada gösterilecektir.', + }, + variableInspect: { + trigger: { + clear: 'Açık', + running: 'Önbellek çalışma durumu', + normal: 'Değişken İncele', + cached: 'Önbelleklenmiş değişkenleri görüntüle', + stop: 'Koşmayı durdur', + }, + envNode: 'Çevre', + title: 'Değişken İncele', + edited: 'Düzenlenmiş', + chatNode: 'Konuşma', + resetConversationVar: 'Konuşma değişkenini varsayılan değere sıfırla', + emptyLink: 'Daha fazla öğrenin', + clearAll: 'Hepsini sıfırla', + systemNode: 'Sistem', + clearNode: 'Önbelleklenmiş değişkeni temizle', + reset: 'Son çalıştırma değerine sıfırla', + view: 'Günlüğü görüntüle', + emptyTip: 'Bir düğümü kanvas üzerinde geçtikten veya bir düğümü adım adım çalıştırdıktan sonra, Düğüm Değişkeni\'ndeki mevcut değeri Değişken İncele\'de görüntüleyebilirsiniz.', + }, + lastRunTab: 'Son Koşu', + settingsTab: 'Ayarlar', + }, } export default translation diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index 7e410ffef9..70bbebe37e 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Напишіть вступне повідомлення', // Write opener placeholder: 'Напишіть тут своє вступне повідомлення, ви можете використовувати змінні, спробуйте ввести {{variable}}.', // Write your opener message here... openingQuestion: 'Відкриваючі питання', // Opening Questions + openingQuestionPlaceholder: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}.', noDataPlaceHolder: 'Початок розмови з користувачем може допомогти ШІ встановити більш тісний зв’язок з ним у розмовних застосунках.', // ... conversational applications. varTip: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}', // You can use variables, try type {{variable}} tooShort: 'Для створення вступних зауважень для розмови потрібно принаймні 20 слів вступного запиту.', // ... are required to generate an opening remarks for the conversation. diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 6e5ff5dc74..9786fd36db 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -93,6 +93,7 @@ const translation = { advancedShortDescription: 'Робочий процес, вдосконалений для багатоетапних чатів', completionUserDescription: 'Швидко створюйте помічника зі штучним інтелектом для завдань із генерації тексту за допомогою простої конфігурації.', workflowUserDescription: 'ізуально створюйте автономні ШІ-процеси з простотою перетягування.', + dropDSLToCreateApp: 'Перетягніть файл DSL сюди, щоб створити додаток', }, editApp: 'Редагувати інформацію', editAppTitle: 'Редагувати інформацію про додаток', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Налаштуйте провайдера для увімкнення відстеження', moreProvider: 'Більше провайдерів', }, + arize: { + title: 'Arize', + description: 'Спостережуваність LLM корпоративного рівня, онлайн та офлайн оцінювання, моніторинг та експерименти—на основі OpenTelemetry. Спеціально розроблено для застосунків на базі LLM та агентів.', + }, + phoenix: { + title: 'Phoenix', + description: 'Відкрита та заснована на OpenTelemetry платформа для спостережуваності, оцінювання, інженерії підказок та експериментів для ваших робочих процесів та агентів LLM.', + }, langsmith: { title: 'LangSmith', description: 'Універсальна платформа розробника для кожного етапу життєвого циклу додатку на основі LLM.', @@ -163,6 +172,7 @@ const translation = { title: 'Ткати', description: 'Weave є платформою з відкритим кодом для оцінки, тестування та моніторингу LLM додатків.', }, + aliyun: {}, }, answerIcon: { title: 'Використовуйте піктограму web app для заміни 🤖', diff --git a/web/i18n/uk-UA/common.ts b/web/i18n/uk-UA/common.ts index a80e308b78..e15bf1bfe4 100644 --- a/web/i18n/uk-UA/common.ts +++ b/web/i18n/uk-UA/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'Не вдалося завантажити. Будь ласка, спробуйте ще раз пізніше.', more: 'Більше', downloadSuccess: 'Завантаження завершено.', + deSelectAll: 'Вимкнути все', + selectAll: 'Вибрати все', }, placeholder: { input: 'Будь ласка, введіть текст', @@ -468,7 +470,6 @@ const translation = { apiBasedExtension: { title: 'API-розширення забезпечують централізоване керування API, спрощуючи конфігурацію для зручного використання в різних програмах Dify.', link: 'Дізнайтеся, як розробити власне розширення API.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Додати розширення API', selector: { title: 'Розширення API', diff --git a/web/i18n/uk-UA/dataset-creation.ts b/web/i18n/uk-UA/dataset-creation.ts index 18b5553be7..72b7a8c05c 100644 --- a/web/i18n/uk-UA/dataset-creation.ts +++ b/web/i18n/uk-UA/dataset-creation.ts @@ -60,7 +60,6 @@ const translation = { unknownError: 'Невідома помилка', maxDepth: 'Максимальна глибина', crawlSubPage: 'Сканування підсторінок', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', preview: 'Попередній перегляд', fireCrawlNotConfigured: 'Firecrawl не налаштовано', includeOnlyPaths: 'Включати лише контури', @@ -88,7 +87,6 @@ const translation = { configureFirecrawl: 'Налаштування Firecrawl', configureWatercrawl: 'Налаштування Watercrawl', watercrawlTitle: 'Витягуйте веб-контент за допомогою Watercrawl', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', watercrawlDoc: 'Документація Watercrawl', }, cancel: 'Скасувати', diff --git a/web/i18n/uk-UA/dataset-documents.ts b/web/i18n/uk-UA/dataset-documents.ts index da012cbb57..27642f1994 100644 --- a/web/i18n/uk-UA/dataset-documents.ts +++ b/web/i18n/uk-UA/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Видалити', enableWarning: 'Архівований файл неможливо активувати', sync: 'Синхронізувати', + pause: 'Пауза', + resume: 'Продовжити', }, index: { enable: 'Активувати', @@ -389,6 +391,8 @@ const translation = { regenerationSuccessMessage: 'Ви можете закрити це вікно.', expandChunks: 'Розгортання фрагментів', regenerationConfirmTitle: 'Хочете регенерувати дитячі шматки?', + keywordEmpty: 'Ключове слово не може бути порожнім', + keywordDuplicate: 'Ключове слово вже існує', }, } diff --git a/web/i18n/uk-UA/dataset.ts b/web/i18n/uk-UA/dataset.ts index 84bc8d9ad1..cfa85c950c 100644 --- a/web/i18n/uk-UA/dataset.ts +++ b/web/i18n/uk-UA/dataset.ts @@ -180,6 +180,7 @@ const translation = { checkName: { empty: 'Ім\'я метаданих не може бути порожнім', invalid: 'Ім\'я метаданих може містити лише малі літери, цифри та підкреслення, і повинно починатися з малої літери', + tooLong: 'Назва метаданих не може перевищувати {{max}} символів', }, batchEditMetadata: { editMetadata: 'Редагувати метадані', diff --git a/web/i18n/uk-UA/plugin.ts b/web/i18n/uk-UA/plugin.ts index ebc3d927d3..a09e17fd3d 100644 --- a/web/i18n/uk-UA/plugin.ts +++ b/web/i18n/uk-UA/plugin.ts @@ -137,6 +137,7 @@ const translation = { installFailed: 'Не вдалося встановити', installedSuccessfully: 'Монтаж успішний', next: 'Наступний', + installWarning: 'Цей плагін не можна установити.', }, installFromGitHub: { selectVersionPlaceholder: 'Будь ласка, оберіть версію', @@ -192,7 +193,6 @@ const translation = { installing: 'Встановлення плагінів {{installingLength}}, 0 виконано.', installingWithSuccess: 'Встановлення плагінів {{installingLength}}, успіх {{successLength}}.', }, - submitPlugin: 'Надіслати плагін', from: 'Від', searchInMarketplace: 'Пошук у Marketplace', endpointsEnabled: '{{num}} наборів кінцевих точок увімкнено', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Поточна версія Dify не сумісна з цим плагіном, будь ласка, оновіть до мінімальної версії: {{minimalDifyVersion}}', requestAPlugin: 'Запросити плагін', + publishPlugins: 'Публікація плагінів', } export default translation diff --git a/web/i18n/uk-UA/tools.ts b/web/i18n/uk-UA/tools.ts index d390b500d3..535c17b1ef 100644 --- a/web/i18n/uk-UA/tools.ts +++ b/web/i18n/uk-UA/tools.ts @@ -142,16 +142,92 @@ const translation = { added: 'Додано', type: 'тип', manageInTools: 'Керування в інструментах', - emptyTip: 'Перейдіть до розділу "Робочий процес -> Опублікувати як інструмент"', - emptyTitle: 'Немає доступного інструменту для роботи з робочими процесами', - emptyTitleCustom: 'Немає доступного спеціального інструменту', - emptyTipCustom: 'Створення власного інструмента', + custom: { + title: 'Немає доступного користувацького інструмента', + tip: 'Створити користувацький інструмент', + }, + workflow: { + title: 'Немає доступного інструмента робочого процесу', + tip: 'Опублікуйте робочі процеси як інструменти в Studio', + }, + mcp: { + title: 'Немає доступного інструмента MCP', + tip: 'Додати сервер MCP', + }, + agent: { + title: 'Немає доступної стратегії агента', + }, }, openInStudio: 'Відкрити в Студії', customToolTip: 'Дізнайтеся більше про користувацькі інструменти Dify', toolNameUsageTip: 'Ім\'я виклику інструменту для міркувань і підказок агента', copyToolName: 'Ім\'я копії', noTools: 'Інструментів не знайдено', + mcp: { + create: { + cardTitle: 'Додати сервер MCP (HTTP)', + cardLink: 'Дізнатися більше про інтеграцію сервера MCP', + }, + noConfigured: 'Сервер не налаштовано', + updateTime: 'Оновлено', + toolsCount: '{count} інструментів', + noTools: 'Інструменти відсутні', + modal: { + title: 'Додати сервер MCP (HTTP)', + editTitle: 'Редагувати сервер MCP (HTTP)', + name: 'Назва та значок', + namePlaceholder: 'Назвіть ваш сервер MCP', + serverUrl: 'URL сервера', + serverUrlPlaceholder: 'URL кінцевої точки сервера', + serverUrlWarning: 'Оновлення адреси сервера може порушити роботу додатків, що залежать від нього', + serverIdentifier: 'Ідентифікатор сервера', + serverIdentifierTip: 'Унікальний ідентифікатор сервера MCP у робочому просторі. Лише малі літери, цифри, підкреслення та дефіси. До 24 символів.', + serverIdentifierPlaceholder: 'Унікальний ідентифікатор, напр. my-mcp-server', + serverIdentifierWarning: 'Після зміни ID існуючі додатки не зможуть розпізнати сервер', + cancel: 'Скасувати', + save: 'Зберегти', + confirm: 'Додати та Авторизувати', + }, + delete: 'Видалити сервер MCP', + deleteConfirmTitle: 'Видалити {mcp}?', + operation: { + edit: 'Редагувати', + remove: 'Видалити', + }, + authorize: 'Авторизувати', + authorizing: 'Авторизація...', + authorizingRequired: 'Потрібна авторизація', + authorizeTip: 'Після авторизації інструменти відображатимуться тут.', + update: 'Оновити', + updating: 'Оновлення...', + gettingTools: 'Отримання інструментів...', + updateTools: 'Оновлення інструментів...', + toolsEmpty: 'Інструменти не завантажено', + getTools: 'Отримати інструменти', + toolUpdateConfirmTitle: 'Оновити список інструментів', + toolUpdateConfirmContent: 'Оновлення списку інструментів може вплинути на існуючі додатки. Продовжити?', + toolsNum: '{count} інструментів включено', + onlyTool: '1 інструмент включено', + identifier: 'Ідентифікатор сервера (Натисніть, щоб скопіювати)', + server: { + title: 'Сервер MCP', + url: 'URL сервера', + reGen: 'Згенерувати URL сервера знову?', + addDescription: 'Додати опис', + edit: 'Редагувати опис', + modal: { + addTitle: 'Додайте опис для активації сервера MCP', + editTitle: 'Редагувати опис', + description: 'Опис', + descriptionPlaceholder: 'Поясніть функціонал інструменту та його використання LLM', + parameters: 'Параметри', + parametersTip: 'Додайте описи параметрів, щоб допомогти LLM зрозуміти їх призначення та обмеження.', + parametersPlaceholder: 'Призначення та обмеження параметра', + confirm: 'Активувати сервер MCP', + }, + publishTip: 'Додаток не опубліковано. Спочатку опублікуйте додаток.', + }, + }, } export default translation diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index bbe32c22c7..dd61582129 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Встановити значення змінної', needConnectTip: 'Цей крок ні до чого не підключений', maxTreeDepth: 'Максимальний ліміт {{depth}} вузлів на гілку', - needEndNode: 'Потрібно додати кінцевий блок', - needAnswerNode: 'Потрібно додати блок відповіді', workflowProcess: 'Процес робочого потоку', notRunning: 'Ще не запущено', previewPlaceholder: 'Введіть вміст у поле нижче, щоб розпочати налагодження чат-бота', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Дізнатися більше', copy: 'Копіювати', duplicate: 'Дублювати', - addBlock: 'Додати блок', pasteHere: 'Вставити сюди', pointerMode: 'Режим вказівника', handMode: 'Ручний режим', @@ -115,6 +112,9 @@ const translation = { exportImage: 'Експорт зображення', exportSVG: 'Експортувати як SVG', exportJPEG: 'Експортувати як JPEG', + addBlock: 'Додати вузол', + needEndNode: 'Необхідно додати кінцевий вузол', + needAnswerNode: 'Вузол Відповіді повинен бути доданий', }, env: { envPanelTitle: 'Змінні середовища', @@ -129,6 +129,8 @@ const translation = { value: 'Значення', valuePlaceholder: 'значення середовища', secretTip: 'Використовується для визначення конфіденційної інформації або даних, з налаштуваннями DSL, сконфігурованими для запобігання витоку.', + description: 'Опис', + descriptionPlaceholder: 'Опишіть змінну', }, export: { title: 'Експортувати секретні змінні середовища?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} кроки вперед', sessionStart: 'Початок сесії', currentState: 'Поточний стан', - nodeTitleChange: 'Назву блоку змінено', - nodeDescriptionChange: 'Опис блоку змінено', - nodeDragStop: 'Блок переміщено', - nodeChange: 'Блок змінено', - nodeConnect: 'Блок підключено', - nodePaste: 'Блок вставлено', - nodeDelete: 'Блок видалено', - nodeAdd: 'Блок додано', - nodeResize: 'Розмір блоку змінено', noteAdd: 'Додано нотатку', noteChange: 'Нотатку змінено', noteDelete: 'Нотатку видалено', - edgeDelete: 'Блок відключено', + nodeTitleChange: 'Заголовок вузла змінено', + nodeResize: 'Вузол змінив розмір', + nodePaste: 'Вузол вставлений', + nodeDelete: 'Вузол видалено', + nodeDragStop: 'Вузол переміщено', + edgeDelete: 'Вузол відключено', + nodeChange: 'Вузол змінився', + nodeAdd: 'Додано вузол', + nodeDescriptionChange: 'Опис вузла змінено', + nodeConnect: 'Вузол підключено', }, errorMsg: { fieldRequired: '{{field}} є обов\'язковим', @@ -217,8 +219,6 @@ const translation = { loop: 'Петля', }, tabs: { - 'searchBlock': 'Пошук блоку', - 'blocks': 'Блоки', 'tools': 'Інструменти', 'allTool': 'Усі', 'builtInTool': 'Вбудовані', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Інструмент пошуку', 'plugin': 'Плагін', 'agent': 'Стратегія агента', + 'blocks': 'Вузли', + 'searchBlock': 'Пошуковий вузол', }, blocks: { 'start': 'Початок', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Поле введення користувача', - changeBlock: 'Змінити блок', helpLink: 'Посилання на допомогу', about: 'Про', createdBy: 'Створено ', nextStep: 'Наступний крок', - addNextStep: 'Додати наступний блок у цей робочий потік', - selectNextStep: 'Вибрати наступний блок', runThisStep: 'Запустити цей крок', checklist: 'Контрольний список', checklistTip: 'Переконайтеся, що всі проблеми вирішені перед публікацією', checklistResolved: 'Всі проблеми вирішені', - organizeBlocks: 'Організувати блоки', change: 'Змінити', optional: '(необов\'язково)', moveToThisNode: 'Перемістіть до цього вузла', + organizeBlocks: 'Організуйте вузли', + changeBlock: 'Змінити вузол', + selectNextStep: 'Виберіть наступний крок', + addNextStep: 'Додайте наступний крок у цей робочий процес', + minimize: 'Вийти з повноекранного режиму', + maximize: 'Максимізувати полотно', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { retryFailedTimes: '{{times}} повторні спроби не вдалися', retryTimes: 'Повторіть спробу {{times}} у разі невдачі', }, + typeSwitch: {}, }, start: { required: 'обов\'язковий', @@ -535,6 +540,10 @@ const translation = { title: 'Імпорт з cURL', placeholder: 'Вставте сюди рядок cURL', }, + verifySSL: { + title: 'Перевірити SSL сертифікат', + warningTooltip: 'Вимкнення перевірки SSL не рекомендується для виробничих середовищ. Це слід використовувати лише в розробці або тестуванні, оскільки це робить з\'єднання вразливим до загроз безпеці, таких як атаки «людина посередині».', + }, }, code: { inputVars: 'Вхідні змінні', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Вхідні змінні', outputVars: { className: 'Назва класу', + usage: 'Інформація про використання моделі', }, class: 'Клас', classNamePlaceholder: 'Напишіть назву вашого класу', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Вхідна змінна', + outputVars: { + isSuccess: 'Є успіх. У разі успіху значення 1, у разі невдачі значення 0.', + errorReason: 'Причина помилки', + usage: 'Інформація про використання моделі', + }, extractParameters: 'Витягти параметри', importFromTool: 'Імпорт з інструментів', addExtractParameter: 'Додати параметр витягування', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Розширене налаштування', reasoningMode: 'Режим інференції', reasoningModeTip: 'Ви можете вибрати відповідний режим інференції залежно від здатності моделі реагувати на інструкції щодо викликів функцій або запитів.', - isSuccess: 'Є успіх. У разі успіху значення 1, у разі невдачі значення 0.', - errorReason: 'Причина помилки', }, iteration: { deleteTitle: 'Видалити вузол ітерації?', @@ -917,6 +930,35 @@ const translation = { nameThisVersion: 'Назвіть цю версію', latest: 'Останні новини', }, + debug: { + noData: { + runThisNode: 'Запустіть цей вузол', + description: 'Результати останнього запуску будуть відображені тут', + }, + variableInspect: { + trigger: { + stop: 'Зупинись бігти', + normal: 'Перевірка змінних', + clear: 'Чіткий', + cached: 'Переглянути кешовані змінні', + running: 'Кешування статусу виконання', + }, + systemNode: 'Система', + view: 'Переглянути журнал', + title: 'Перевірка змінних', + edited: 'Редагований', + emptyLink: 'Дізнайтеся більше', + clearNode: 'Очистити кешовану змінну', + envNode: 'Навколишнє середовище', + reset: 'Скинути до значення останнього запуску', + clearAll: 'Скиньте все', + chatNode: 'Розмова', + resetConversationVar: 'Скинути змінну розмови на значення за замовчуванням', + emptyTip: 'Після переходу через вузол на полотні або виконання вузла поетапно, ви можете переглянути поточне значення змінної вузла у Перевірці змінних.', + }, + lastRunTab: 'Останній запуск', + settingsTab: 'Налаштування', + }, } export default translation diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index c091cb5abb..cd57f78e79 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Viết câu mở đầu', placeholder: 'Viết thông điệp mở đầu của bạn ở đây, bạn có thể sử dụng biến, hãy thử nhập {{biến}}.', openingQuestion: 'Câu hỏi mở đầu', + openingQuestionPlaceholder: 'Bạn có thể sử dụng biến, hãy thử nhập {{variable}}.', noDataPlaceHolder: 'Bắt đầu cuộc trò chuyện với người dùng có thể giúp AI thiết lập mối quan hệ gần gũi hơn với họ trong các ứng dụng trò chuyện.', varTip: 'Bạn có thể sử dụng biến, hãy thử nhập {{biến}}', tooShort: 'Cần ít nhất 20 từ trong lời nhắc ban đầu để tạo ra các câu mở đầu cho cuộc trò chuyện.', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index c5f1a7496d..d8f80d9df0 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -93,6 +93,7 @@ const translation = { learnMore: 'Tìm hiểu thêm', completionShortDescription: 'Trợ lý AI cho các tác vụ tạo văn bản', completionUserDescription: 'Nhanh chóng xây dựng trợ lý AI cho các tác vụ tạo văn bản với cấu hình đơn giản.', + dropDSLToCreateApp: 'Kéo tệp DSL vào đây để tạo ứng dụng', }, editApp: 'Chỉnh sửa thông tin', editAppTitle: 'Chỉnh sửa thông tin ứng dụng', @@ -135,6 +136,14 @@ const translation = { notConfigured: 'Cấu hình nhà cung cấp để bật theo dõi', moreProvider: 'Thêm nhà cung cấp', }, + arize: { + title: 'Arize', + description: 'Khả năng quan sát LLM cấp doanh nghiệp, đánh giá trực tuyến và ngoại tuyến, giám sát và thử nghiệm—được hỗ trợ bởi OpenTelemetry. Được thiết kế đặc biệt cho các ứng dụng dựa trên LLM và tác nhân.', + }, + phoenix: { + title: 'Phoenix', + description: 'Nền tảng mã nguồn mở và dựa trên OpenTelemetry cho khả năng quan sát, đánh giá, kỹ thuật prompt và thử nghiệm cho quy trình làm việc và tác nhân LLM của bạn.', + }, langsmith: { title: 'LangSmith', description: 'Nền tảng phát triển tất cả trong một cho mọi bước của vòng đời ứng dụng được hỗ trợ bởi LLM.', @@ -163,6 +172,7 @@ const translation = { title: 'Dệt', description: 'Weave là một nền tảng mã nguồn mở để đánh giá, thử nghiệm và giám sát các ứng dụng LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong ứng dụng được chia sẻ hay không', diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts index 323ca60152..3ab959a25f 100644 --- a/web/i18n/vi-VN/common.ts +++ b/web/i18n/vi-VN/common.ts @@ -58,6 +58,8 @@ const translation = { downloadFailed: 'Tải xuống thất bại. Vui lòng thử lại sau.', format: 'Định dạng', downloadSuccess: 'Tải xuống đã hoàn thành.', + deSelectAll: 'Bỏ chọn tất cả', + selectAll: 'Chọn Tất Cả', }, placeholder: { input: 'Vui lòng nhập', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'Các tiện ích API cung cấp quản lý API tập trung, giúp cấu hình dễ dàng sử dụng trên các ứng dụng của Dify.', link: 'Tìm hiểu cách phát triển Phần mở rộng API của riêng bạn.', - linkUrl: 'https://docs.dify.ai/en/guides/extension/api-based-extension/README', add: 'Thêm Phần mở rộng API', selector: { title: 'Phần mở rộng API', diff --git a/web/i18n/vi-VN/dataset-creation.ts b/web/i18n/vi-VN/dataset-creation.ts index f909a51770..34ef374500 100644 --- a/web/i18n/vi-VN/dataset-creation.ts +++ b/web/i18n/vi-VN/dataset-creation.ts @@ -63,7 +63,6 @@ const translation = { unknownError: 'Lỗi không xác định', extractOnlyMainContent: 'Chỉ trích xuất nội dung chính (không có đầu trang, điều hướng, chân trang, v.v.)', exceptionErrorTitle: 'Một ngoại lệ xảy ra trong khi chạy tác vụ Firecrawl:', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', selectAll: 'Chọn tất cả', firecrawlTitle: 'Trích xuất nội dung web bằng 🔥Firecrawl', totalPageScraped: 'Tổng số trang được cạo:', @@ -86,7 +85,6 @@ const translation = { configureFirecrawl: 'Cấu hình Firecrawl', configureJinaReader: 'Cấu hình Jina Reader', waterCrawlNotConfiguredDescription: 'Cấu hình Watercrawl với khóa API để sử dụng nó.', - watercrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', watercrawlTitle: 'Trích xuất nội dung web bằng Watercrawl', watercrawlDoc: 'Tài liệu Watercrawl', waterCrawlNotConfigured: 'Watercrawl chưa được cấu hình', diff --git a/web/i18n/vi-VN/dataset-documents.ts b/web/i18n/vi-VN/dataset-documents.ts index 6e13c1185f..94d4dec556 100644 --- a/web/i18n/vi-VN/dataset-documents.ts +++ b/web/i18n/vi-VN/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: 'Xóa', enableWarning: 'Tệp đã lưu trữ không thể được kích hoạt', sync: 'Đồng bộ', + pause: 'Tạm dừng', + resume: 'Tiếp tục', }, index: { enable: 'Kích hoạt', @@ -388,6 +390,8 @@ const translation = { clearFilter: 'Bộ lọc rõ ràng', chunk: 'Khúc', edited: 'EDITED', + keywordDuplicate: 'Từ khóa đã tồn tại', + keywordEmpty: 'Từ khóa không được để trống', }, } diff --git a/web/i18n/vi-VN/dataset.ts b/web/i18n/vi-VN/dataset.ts index 4227d712a0..41260c982c 100644 --- a/web/i18n/vi-VN/dataset.ts +++ b/web/i18n/vi-VN/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { invalid: 'Tên siêu dữ liệu chỉ có thể chứa chữ cái thường, số và dấu gạch dưới, và phải bắt đầu bằng một chữ cái thường.', empty: 'Tên siêu dữ liệu không được để trống', + tooLong: 'Tên siêu dữ liệu không được vượt quá {{max}} ký tự', }, batchEditMetadata: { applyToAllSelectDocumentTip: 'Tự động tạo tất cả các siêu dữ liệu đã chỉnh sửa và mới cho tất cả các tài liệu được chọn, nếu không, việc chỉnh sửa siêu dữ liệu sẽ chỉ áp dụng cho các tài liệu có nó.', diff --git a/web/i18n/vi-VN/plugin.ts b/web/i18n/vi-VN/plugin.ts index 889d79f0ca..fa996634d8 100644 --- a/web/i18n/vi-VN/plugin.ts +++ b/web/i18n/vi-VN/plugin.ts @@ -137,6 +137,7 @@ const translation = { installComplete: 'Cài đặt hoàn tất', back: 'Lưng', pluginLoadError: 'Lỗi tải plugin', + installWarning: 'Plugin này không được phép cài đặt.', }, installFromGitHub: { installFailed: 'Cài đặt không thành công', @@ -198,7 +199,6 @@ const translation = { endpointsEnabled: '{{num}} bộ điểm cuối được kích hoạt', install: '{{num}} lượt cài đặt', findMoreInMarketplace: 'Tìm thêm trong Marketplace', - submitPlugin: 'Gửi plugin', search: 'Tìm kiếm', searchCategories: 'Danh mục tìm kiếm', installPlugin: 'Cài đặt plugin', @@ -212,6 +212,7 @@ const translation = { }, difyVersionNotCompatible: 'Phiên bản Dify hiện tại không tương thích với plugin này, vui lòng nâng cấp lên phiên bản tối thiểu cần thiết: {{minimalDifyVersion}}', requestAPlugin: 'Yêu cầu một plugin', + publishPlugins: 'Xuất bản plugin', } export default translation diff --git a/web/i18n/vi-VN/tools.ts b/web/i18n/vi-VN/tools.ts index ec4665cbf5..4f3893cade 100644 --- a/web/i18n/vi-VN/tools.ts +++ b/web/i18n/vi-VN/tools.ts @@ -142,16 +142,92 @@ const translation = { type: 'kiểu', add: 'thêm', added: 'Thêm', - emptyTip: 'Đi tới "Quy trình làm việc -> Xuất bản dưới dạng công cụ"', - emptyTitle: 'Không có sẵn công cụ quy trình làm việc', - emptyTitleCustom: 'Không có công cụ tùy chỉnh nào có sẵn', - emptyTipCustom: 'Tạo công cụ tùy chỉnh', + custom: { + title: 'Không có công cụ tùy chỉnh nào', + tip: 'Tạo một công cụ tùy chỉnh', + }, + workflow: { + title: 'Không có công cụ quy trình nào', + tip: 'Xuất bản các quy trình dưới dạng công cụ trong Studio', + }, + mcp: { + title: 'Không có công cụ MCP nào', + tip: 'Thêm máy chủ MCP', + }, + agent: { + title: 'Không có chiến lược đại lý nào', + }, }, toolNameUsageTip: 'Tên cuộc gọi công cụ để lý luận và nhắc nhở tổng đài viên', customToolTip: 'Tìm hiểu thêm về các công cụ tùy chỉnh Dify', openInStudio: 'Mở trong Studio', noTools: 'Không tìm thấy công cụ', copyToolName: 'Sao chép tên', + mcp: { + create: { + cardTitle: 'Thêm Máy chủ MCP (HTTP)', + cardLink: 'Tìm hiểu thêm về tích hợp máy chủ MCP', + }, + noConfigured: 'Máy chủ Chưa được Cấu hình', + updateTime: 'Cập nhật', + toolsCount: '{count} công cụ', + noTools: 'Không có công cụ nào', + modal: { + title: 'Thêm Máy chủ MCP (HTTP)', + editTitle: 'Sửa Máy chủ MCP (HTTP)', + name: 'Tên & Biểu tượng', + namePlaceholder: 'Đặt tên máy chủ MCP', + serverUrl: 'URL Máy chủ', + serverUrlPlaceholder: 'URL đến điểm cuối máy chủ', + serverUrlWarning: 'Cập nhật địa chỉ máy chủ có thể làm gián đoạn ứng dụng phụ thuộc vào máy chủ này', + serverIdentifier: 'Định danh Máy chủ', + serverIdentifierTip: 'Định danh duy nhất cho máy chủ MCP trong không gian làm việc. Chỉ chữ thường, số, gạch dưới và gạch ngang. Tối đa 24 ký tự.', + serverIdentifierPlaceholder: 'Định danh duy nhất, VD: my-mcp-server', + serverIdentifierWarning: 'Máy chủ sẽ không được nhận diện bởi ứng dụng hiện có sau khi thay đổi ID', + cancel: 'Hủy', + save: 'Lưu', + confirm: 'Thêm & Ủy quyền', + }, + delete: 'Xóa Máy chủ MCP', + deleteConfirmTitle: 'Xóa {mcp}?', + operation: { + edit: 'Sửa', + remove: 'Xóa', + }, + authorize: 'Ủy quyền', + authorizing: 'Đang ủy quyền...', + authorizingRequired: 'Cần ủy quyền', + authorizeTip: 'Sau khi ủy quyền, công cụ sẽ hiển thị tại đây.', + update: 'Cập nhật', + updating: 'Đang cập nhật...', + gettingTools: 'Đang lấy công cụ...', + updateTools: 'Đang cập nhật công cụ...', + toolsEmpty: 'Công cụ chưa tải', + getTools: 'Lấy công cụ', + toolUpdateConfirmTitle: 'Cập nhật Danh sách Công cụ', + toolUpdateConfirmContent: 'Cập nhật danh sách công cụ có thể ảnh hưởng ứng dụng hiện có. Tiếp tục?', + toolsNum: 'Bao gồm {count} công cụ', + onlyTool: 'Bao gồm 1 công cụ', + identifier: 'Định danh Máy chủ (Nhấn để Sao chép)', + server: { + title: 'Máy chủ MCP', + url: 'URL Máy chủ', + reGen: 'Tạo lại URL máy chủ?', + addDescription: 'Thêm mô tả', + edit: 'Sửa mô tả', + modal: { + addTitle: 'Thêm mô tả để kích hoạt máy chủ MCP', + editTitle: 'Sửa mô tả', + description: 'Mô tả', + descriptionPlaceholder: 'Giải thích chức năng công cụ và cách LLM sử dụng', + parameters: 'Tham số', + parametersTip: 'Thêm mô tả cho từng tham số để giúp LLM hiểu mục đích và ràng buộc.', + parametersPlaceholder: 'Mục đích và ràng buộc của tham số', + confirm: 'Kích hoạt Máy chủ MCP', + }, + publishTip: 'Ứng dụng chưa xuất bản. Vui lòng xuất bản ứng dụng trước.', + }, + }, } export default translation diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 448a4657a4..05cd279728 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -38,8 +38,6 @@ const translation = { setVarValuePlaceholder: 'Đặt giá trị biến', needConnectTip: 'Bước này không được kết nối với bất kỳ điều gì', maxTreeDepth: 'Giới hạn tối đa {{depth}} nút trên mỗi nhánh', - needEndNode: 'Phải thêm khối Kết thúc', - needAnswerNode: 'Phải thêm khối Trả lời', workflowProcess: 'Quy trình làm việc', notRunning: 'Chưa chạy', previewPlaceholder: 'Nhập nội dung vào hộp bên dưới để bắt đầu gỡ lỗi Chatbot', @@ -58,7 +56,6 @@ const translation = { learnMore: 'Tìm hiểu thêm', copy: 'Sao chép', duplicate: 'Nhân bản', - addBlock: 'Thêm khối', pasteHere: 'Dán vào đây', pointerMode: 'Chế độ con trỏ', handMode: 'Chế độ tay', @@ -115,6 +112,9 @@ const translation = { noExist: 'Không có biến như vậy', exportJPEG: 'Xuất dưới dạng JPEG', referenceVar: 'Biến tham chiếu', + needAnswerNode: 'Nút Trả lời phải được thêm vào', + addBlock: 'Thêm Node', + needEndNode: 'Nút Kết thúc phải được thêm vào', }, env: { envPanelTitle: 'Biến Môi Trường', @@ -129,6 +129,8 @@ const translation = { value: 'Giá trị', valuePlaceholder: 'giá trị môi trường', secretTip: 'Được sử dụng để xác định thông tin hoặc dữ liệu nhạy cảm, với cài đặt DSL được cấu hình để ngăn chặn rò rỉ.', + description: 'Mô tả', + descriptionPlaceholder: 'Mô tả biến', }, export: { title: 'Xuất biến môi trường bí mật?', @@ -176,19 +178,19 @@ const translation = { stepForward_other: '{{count}} bước tiến', sessionStart: 'Bắt đầu phiên', currentState: 'Trạng thái hiện tại', - nodeTitleChange: 'Tiêu đề khối đã thay đổi', - nodeDescriptionChange: 'Mô tả khối đã thay đổi', - nodeDragStop: 'Khối đã di chuyển', - nodeChange: 'Khối đã thay đổi', - nodeConnect: 'Khối đã kết nối', - nodePaste: 'Khối đã dán', - nodeDelete: 'Khối đã xóa', - nodeAdd: 'Khối đã thêm', - nodeResize: 'Khối đã thay đổi kích thước', noteAdd: 'Ghi chú đã thêm', noteChange: 'Ghi chú đã thay đổi', noteDelete: 'Ghi chú đã xóa', - edgeDelete: 'Khối đã ngắt kết nối', + nodeAdd: 'Đã thêm nút', + nodeChange: 'Node đã thay đổi', + nodeDescriptionChange: 'Mô tả nút đã thay đổi', + nodeTitleChange: 'Tiêu đề nút đã được thay đổi', + nodeDelete: 'Nút đã bị xóa', + nodeDragStop: 'Nút đã được di chuyển', + nodeConnect: 'Nút đã kết nối', + nodeResize: 'Kích thước nút đã được thay đổi', + nodePaste: 'Node đã dán', + edgeDelete: 'Nút đã bị ngắt kết nối', }, errorMsg: { fieldRequired: '{{field}} là bắt buộc', @@ -217,8 +219,6 @@ const translation = { loop: 'Vòng', }, tabs: { - 'searchBlock': 'Tìm kiếm khối', - 'blocks': 'Khối', 'tools': 'Công cụ', 'allTool': 'Tất cả', 'builtInTool': 'Tích hợp sẵn', @@ -232,6 +232,8 @@ const translation = { 'searchTool': 'Công cụ tìm kiếm', 'agent': 'Chiến lược đại lý', 'plugin': 'Plugin', + 'blocks': 'Nút', + 'searchBlock': 'Tìm kiếm nút', }, blocks: { 'start': 'Bắt đầu', @@ -288,21 +290,23 @@ const translation = { }, panel: { userInputField: 'Trường đầu vào của người dùng', - changeBlock: 'Thay đổi khối', helpLink: 'Liên kết trợ giúp', about: 'Giới thiệu', createdBy: 'Tạo bởi ', nextStep: 'Bước tiếp theo', - addNextStep: 'Thêm khối tiếp theo trong quy trình làm việc này', - selectNextStep: 'Chọn khối tiếp theo', runThisStep: 'Chạy bước này', checklist: 'Danh sách kiểm tra', checklistTip: 'Đảm bảo rằng tất cả các vấn đề đã được giải quyết trước khi xuất bản', checklistResolved: 'Tất cả các vấn đề đã được giải quyết', - organizeBlocks: 'Tổ chức các khối', change: 'Thay đổi', optional: '(tùy chọn)', moveToThisNode: 'Di chuyển đến nút này', + changeBlock: 'Thay đổi Node', + selectNextStep: 'Chọn bước tiếp theo', + organizeBlocks: 'Tổ chức các nút', + addNextStep: 'Thêm bước tiếp theo trong quy trình này', + maximize: 'Tối đa hóa Canvas', + minimize: 'Thoát chế độ toàn màn hình', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { times: 'lần', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'bắt buộc', @@ -535,6 +540,10 @@ const translation = { title: 'Nhập từ cURL', placeholder: 'Dán chuỗi cURL vào đây', }, + verifySSL: { + title: 'Xác thực chứng chỉ SSL', + warningTooltip: 'Việc vô hiệu hóa xác minh SSL không được khuyến khích cho các môi trường sản xuất. Điều này chỉ nên được sử dụng trong phát triển hoặc thử nghiệm, vì nó làm cho kết nối dễ bị tổn thương trước các mối đe dọa an ninh như cuộc tấn công man-in-the-middle.', + }, }, code: { inputVars: 'Biến đầu vào', @@ -667,6 +676,7 @@ const translation = { inputVars: 'Biến đầu vào', outputVars: { className: 'Tên lớp', + usage: 'Thông tin sử dụng mô hình', }, class: 'Lớp', classNamePlaceholder: 'Viết tên lớp của bạn', @@ -680,6 +690,11 @@ const translation = { }, parameterExtractor: { inputVar: 'Biến đầu vào', + outputVars: { + isSuccess: 'Thành công. Khi thành công giá trị là 1, khi thất bại giá trị là 0.', + errorReason: 'Lý do lỗi', + usage: 'Thông tin sử dụng mô hình', + }, extractParameters: 'Trích xuất tham số', importFromTool: 'Nhập từ công cụ', addExtractParameter: 'Thêm tham số trích xuất', @@ -699,8 +714,6 @@ const translation = { advancedSetting: 'Cài đặt nâng cao', reasoningMode: 'Chế độ suy luận', reasoningModeTip: 'Bạn có thể chọn chế độ suy luận phù hợp dựa trên khả năng của mô hình để phản hồi các hướng dẫn về việc gọi hàm hoặc prompt.', - isSuccess: 'Thành công. Khi thành công giá trị là 1, khi thất bại giá trị là 0.', - errorReason: 'Lý do lỗi', }, iteration: { deleteTitle: 'Xóa nút lặp?', @@ -917,6 +930,35 @@ const translation = { restorationTip: 'Sau khi phục hồi phiên bản, bản nháp hiện tại sẽ bị ghi đè.', title: 'Các phiên bản', }, + debug: { + noData: { + runThisNode: 'Chạy nút này', + description: 'Kết quả của lần chạy cuối cùng sẽ được hiển thị ở đây', + }, + variableInspect: { + trigger: { + clear: 'Rõ ràng', + stop: 'Dừng lại', + normal: 'Kiểm tra Biến', + cached: 'Xem các biến được lưu trong bộ nhớ cache', + running: 'Trạng thái đang chạy của bộ nhớ đệm', + }, + envNode: 'Môi trường', + edited: 'Biên soạn', + chatNode: 'Cuộc trò chuyện', + view: 'Xem nhật ký', + clearAll: 'Đặt lại tất cả', + reset: 'Đặt lại thành giá trị của lần chạy cuối cùng', + resetConversationVar: 'Đặt lại biến cuộc trò chuyện về giá trị mặc định', + title: 'Kiểm tra Biến', + systemNode: 'Hệ thống', + clearNode: 'Xóa biến đã được lưu trong bộ nhớ cache', + emptyLink: 'Tìm hiểu thêm', + emptyTip: 'Sau khi bước qua một nút trên canvas hoặc chạy một nút từng bước, bạn có thể xem giá trị hiện tại của biến nút trong Variable Inspect.', + }, + settingsTab: 'Cài đặt', + lastRunTab: 'Chạy Lần Cuối', + }, } export default translation diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 4f84b396d0..3728d86e58 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -436,6 +436,7 @@ const translation = { writeOpener: '编写开场白', placeholder: '在这里写下你的开场白,你可以使用变量,尝试输入 {{variable}}。', openingQuestion: '开场问题', + openingQuestionPlaceholder: '可以使用变量,尝试输入 {{variable}}。', noDataPlaceHolder: '在对话型应用中,让 AI 主动说第一段话可以拉近与用户间的距离。', varTip: '你可以使用变量,试试输入 {{variable}}', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 4ec1e65059..c616c088b1 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -87,6 +87,7 @@ const translation = { appCreateDSLErrorPart3: '当前应用 DSL 版本:', appCreateDSLErrorPart4: '系统支持 DSL 版本:', appCreateFailed: '应用创建失败', + dropDSLToCreateApp: '拖放 DSL 文件到此处创建应用', Confirm: '确认', }, newAppFromTemplate: { @@ -149,6 +150,14 @@ const translation = { notConfigured: '配置提供商以启用追踪', moreProvider: '更多提供商', }, + arize: { + title: 'Arize', + description: '企业级LLM可观测性、在线和离线评估、监控和实验平台,基于OpenTelemetry构建,专为LLM和代理驱动的应用程序设计。', + }, + phoenix: { + title: 'Phoenix', + description: '开源且基于OpenTelemetry的可观测性、评估、提示工程和实验平台,适用于您的LLM工作流程和代理。', + }, langsmith: { title: 'LangSmith', description: '一个全方位的开发者平台,适用于 LLM 驱动应用程序生命周期的每个步骤。', @@ -176,6 +185,10 @@ const translation = { title: '编织', description: 'Weave 是一个开源平台,用于评估、测试和监控大型语言模型应用程序。', }, + aliyun: { + title: '云监控', + description: '阿里云提供的全托管免运维可观测平台,一键开启Dify应用的监控追踪和评估', + }, }, appSelector: { label: '应用', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 00c8a33837..39964bb6b0 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -64,6 +64,8 @@ const translation = { skip: '跳过', format: '格式化', more: '更多', + selectAll: '全选', + deSelectAll: '取消全选', }, errorMsg: { fieldRequired: '{{field}} 为必填项', @@ -484,7 +486,6 @@ const translation = { apiBasedExtension: { title: 'API 扩展提供了一个集中式的 API 管理,在此统一添加 API 配置后,方便在 Dify 上的各类应用中直接使用。', link: '了解如何开发您自己的 API 扩展。', - linkUrl: 'https://docs.dify.ai/zh-hans/guides/extension/api-based-extension', add: '新增 API 扩展', selector: { title: 'API 扩展', diff --git a/web/i18n/zh-Hans/dataset-creation.ts b/web/i18n/zh-Hans/dataset-creation.ts index 6f4c5af7eb..c8d4a82244 100644 --- a/web/i18n/zh-Hans/dataset-creation.ts +++ b/web/i18n/zh-Hans/dataset-creation.ts @@ -79,7 +79,6 @@ const translation = { run: '运行', firecrawlTitle: '使用 🔥Firecrawl 提取网页内容', firecrawlDoc: 'Firecrawl 文档', - firecrawlDocLink: 'https://docs.dify.ai/zh-hans/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', jinaReaderTitle: '将整个站点内容转换为 Markdown 格式', jinaReaderDoc: '了解更多关于 Jina Reader', jinaReaderDocLink: 'https://jina.ai/reader', @@ -100,7 +99,6 @@ const translation = { scrapTimeInfo: '总共在 {{time}}秒 内抓取了 {{total}} 个页面', preview: '预览', maxDepthTooltip: '相对于输入 URL 的最大抓取深度。深度 0 仅抓取输入 URL 本身的页面,深度 1 抓取输入 URL 及其后的一层目录(一个 /),依此类推。', - watercrawlDocLink: '从网站同步', watercrawlDoc: 'Watercrawl 文档', configureWatercrawl: '配置水爬行', watercrawlTitle: '使用 Watercrawl 提取网页内容', diff --git a/web/i18n/zh-Hans/dataset-documents.ts b/web/i18n/zh-Hans/dataset-documents.ts index 5ff1b50f85..659d8c5ee9 100644 --- a/web/i18n/zh-Hans/dataset-documents.ts +++ b/web/i18n/zh-Hans/dataset-documents.ts @@ -30,6 +30,8 @@ const translation = { delete: '删除', enableWarning: '归档的文件无法启用', sync: '同步', + pause: '暂停', + resume: '恢复', }, index: { enable: '启用中', @@ -387,6 +389,8 @@ const translation = { editedAt: '编辑于', expandChunks: '展开分段', collapseChunks: '折叠分段', + keywordEmpty: '关键词不能为空', + keywordDuplicate: '关键词已经存在', }, } diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 9b5691ae24..fa1348c527 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -183,6 +183,7 @@ const translation = { checkName: { empty: '元数据名称不能为空', invalid: '元数据名称只能包含小写字母、数字和下划线,并且必须以小写字母开头', + tooLong: '元数据名称不得超过{{max}}个字符', }, batchEditMetadata: { editMetadata: '编辑元数据', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index db653913a2..89cdddc1e3 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -94,6 +94,7 @@ const translation = { unsupportedTitle: '不支持的 Action', unsupportedContent: '已安装的插件版本不提供这个 action。', unsupportedContent2: '点击切换版本', + unsupportedMCPTool: '当前选定的 Agent 策略插件版本不支持 MCP 工具。', }, configureApp: '应用设置', configureModel: '模型设置', @@ -154,6 +155,7 @@ const translation = { next: '下一步', pluginLoadError: '插件加载错误', pluginLoadErrorDesc: '此插件将不会被安装', + installWarning: '此插件不允许安装。', }, installFromGitHub: { installPlugin: '从 GitHub 安装插件', @@ -210,7 +212,7 @@ const translation = { clearAll: '清除所有', }, requestAPlugin: '申请插件', - submitPlugin: '上传插件', + publishPlugins: '发布插件', difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', } diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 9a573ad308..5c1eb13236 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '添加', added: '已添加', manageInTools: '去工具列表管理', - emptyTitle: '没有可用的工作流工具', - emptyTip: '去“工作流 -> 发布为工具”添加', - emptyTitleCustom: '没有可用的自定义工具', - emptyTipCustom: '创建自定义工具', + custom: { + title: '没有可用的自定义工具', + tip: '创建自定义工具', + }, + workflow: { + title: '没有可用的工作流工具', + tip: '在工作室中发布工作流作为工具', + }, + mcp: { + title: '没有可用的 MCP 工具', + tip: '添加 MCP 服务器', + }, + agent: { + title: '没有可用的 agent 策略', + }, }, createTool: { title: '创建自定义工具', @@ -69,11 +80,15 @@ const translation = { title: '鉴权方法', type: '鉴权类型', keyTooltip: 'HTTP 头部名称,如果你不知道是什么,可以将其保留为 Authorization 或设置为自定义值', + queryParam: '查询参数', + queryParamTooltip: '用于传递 API 密钥查询参数的名称, 如 "https://example.com/test?key=API_KEY" 中的 "key"参数', types: { none: '无', - api_key: 'API Key', + api_key_header: '请求头', + api_key_query: '查询参数', apiKeyPlaceholder: 'HTTP 头部名称,用于传递 API Key', apiValuePlaceholder: '输入 API Key', + queryParamPlaceholder: '查询参数名称,用于传递 API Key', }, key: '键', value: '值', @@ -152,6 +167,71 @@ const translation = { toolNameUsageTip: '工具调用名称,用于 Agent 推理和提示词', copyToolName: '复制名称', noTools: '没有工具', + mcp: { + create: { + cardTitle: '添加 MCP 服务 (HTTP)', + cardLink: '了解更多关于 MCP 服务集成的信息', + }, + noConfigured: '未配置', + updateTime: '更新于', + toolsCount: '{{count}} 个工具', + noTools: '没有可用的工具', + modal: { + title: '添加 MCP 服务 (HTTP)', + editTitle: '修改 MCP 服务 (HTTP)', + name: '名称和图标', + namePlaceholder: '命名你的 MCP 服务', + serverUrl: '服务端点 URL', + serverUrlPlaceholder: '服务端点的 URL', + serverUrlWarning: '修改服务端点 URL 可能会影响使用当前 MCP 的应用。', + serverIdentifier: '服务器标识符', + serverIdentifierTip: '工作空间内服务器的唯一标识。支持小写字母、数字、下划线和连字符,最多 24 个字符。', + serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server', + serverIdentifierWarning: '更改服务器标识符后,现有应用将无法识别此服务器', + cancel: '取消', + save: '保存', + confirm: '添加并授权', + }, + delete: '删除 MCP 服务', + deleteConfirmTitle: '你想要删除 {{mcp}} 吗?', + operation: { + edit: '修改', + remove: '删除', + }, + authorize: '授权', + authorizing: '授权中...', + authorizingRequired: '需要授权', + authorizeTip: '授权后,工具将显示在这里。', + update: '更新', + updating: '更新中', + gettingTools: '获取工具中...', + updateTools: '更新工具中...', + toolsEmpty: '工具未加载', + getTools: '获取工具', + toolUpdateConfirmTitle: '更新工具列表', + toolUpdateConfirmContent: '更新工具列表可能影响现有应用。您想继续吗?', + toolsNum: '包含 {{count}} 个工具', + onlyTool: '包含 1 个工具', + identifier: '服务器标识符 (点击复制)', + server: { + title: 'MCP 服务', + url: '服务端点 URL', + reGen: '你想要重新生成服务端点 URL 吗?', + addDescription: '添加描述', + edit: '编辑描述', + modal: { + addTitle: '添加描述以启用 MCP 服务', + editTitle: '编辑 MCP 服务描述', + description: '描述', + descriptionPlaceholder: '解释此工具的功能以及 LLM 应如何使用它', + parameters: '参数', + parametersTip: '为每个参数添加描述,以帮助 LLM 理解其目的和约束条件。', + parametersPlaceholder: '参数的用途和约束条件', + confirm: '启用 MCP 服务', + }, + publishTip: '应用未发布。请先发布应用。', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 79b9e674fd..6bd202d58f 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -127,6 +127,8 @@ const translation = { value: '值', valuePlaceholder: '变量值', secretTip: '用于定义敏感信息或数据,导出 DSL 时设置了防泄露机制。', + description: '描述', + descriptionPlaceholder: '变量的描述', }, export: { title: '导出 Secret 类型环境变量?', @@ -230,6 +232,8 @@ const translation = { 'utilities': '工具', 'noResult': '未找到匹配项', 'agent': 'Agent 策略', + 'allAdded': '已添加全部', + 'addAll': '添加全部', }, blocks: { 'start': '开始', @@ -308,6 +312,8 @@ const translation = { change: '更改', optional: '(选填)', moveToThisNode: '定位至此节点', + maximize: '最大化画布', + minimize: '退出最大化', }, nodes: { common: { @@ -365,6 +371,10 @@ const translation = { ms: '毫秒', retries: '{{num}} 重试次数', }, + typeSwitch: { + input: '输入值', + variable: '使用变量', + }, }, start: { required: '必填', @@ -541,6 +551,10 @@ const translation = { title: '导入 cURL', placeholder: '粘贴 cURL 字符串', }, + verifySSL: { + title: '验证 SSL 证书', + warningTooltip: '不建议在生产环境中禁用 SSL 验证。这仅应在开发或测试中使用,因为它会使连接容易受到诸如中间人攻击等安全威胁。', + }, }, code: { inputVars: '输入变量', @@ -548,6 +562,7 @@ const translation = { advancedDependencies: '高级依赖', advancedDependenciesTip: '在这里添加一些预加载需要消耗较多时间或非默认内置的依赖包', searchDependencies: '搜索依赖', + syncFunctionSignature: '同步函数签名至代码', }, templateTransform: { inputVars: '输入变量', @@ -654,6 +669,9 @@ const translation = { tool: { authorize: '授权', inputVars: '输入变量', + settings: '设置', + insertPlaceholder1: '键入', + insertPlaceholder2: '插入变量', outputVars: { text: '工具生成的内容', files: { @@ -671,6 +689,7 @@ const translation = { inputVars: '输入变量', outputVars: { className: '分类名称', + usage: '模型用量信息', }, class: '分类', classNamePlaceholder: '输入你的分类名称', @@ -684,6 +703,11 @@ const translation = { }, parameterExtractor: { inputVar: '输入变量', + outputVars: { + isSuccess: '是否成功。成功时值为 1,失败时值为 0。', + errorReason: '错误原因', + usage: '模型用量信息', + }, extractParameters: '提取参数', importFromTool: '从工具导入', addExtractParameter: '添加提取参数', @@ -703,8 +727,6 @@ const translation = { advancedSetting: '高级设置', reasoningMode: '推理模式', reasoningModeTip: '你可以根据模型对于 Function calling 或 Prompt 的指令响应能力选择合适的推理模式', - isSuccess: '是否成功。成功时值为 1,失败时值为 0。', - errorReason: '错误原因', }, iteration: { deleteTitle: '删除迭代节点?', @@ -877,6 +899,8 @@ const translation = { install: '安装', cancel: '取消', }, + clickToViewParameterSchema: '点击查看参数 schema', + parameterSchema: '参数 Schema', }, }, tracing: { @@ -914,6 +938,35 @@ const translation = { updateFailure: '更新失败', }, }, + debug: { + settingsTab: '设置', + lastRunTab: '上次运行', + noData: { + description: '上次运行的结果将显示在这里', + runThisNode: '运行此节点', + }, + variableInspect: { + title: '变量检查', + emptyTip: '在画布上逐步浏览节点或逐步运行节点后,您可以在变量检查中查看节点变量的当前值', + emptyLink: '了解更多', + clearAll: '重置所有', + clearNode: '清除缓存', + resetConversationVar: '重置会话变量为默认值', + view: '查看记录', + edited: '已编辑', + reset: '还原至上一次运行', + trigger: { + normal: '变量检查', + running: '缓存中', + stop: '停止运行', + cached: '查看缓存', + clear: '清除', + }, + envNode: '环境变量', + chatNode: '会话变量', + systemNode: '系统变量', + }, + }, } export default translation diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index 16374992b5..b31b9a9d66 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -313,6 +313,7 @@ const translation = { writeOpener: '編寫開場白', placeholder: '在這裡寫下你的開場白,你可以使用變數,嘗試輸入 {{variable}}。', openingQuestion: '開場問題', + openingQuestionPlaceholder: '可以使用變量,嘗試輸入 {{variable}}。', noDataPlaceHolder: '在對話型應用中,讓 AI 主動說第一段話可以拉近與使用者間的距離。', varTip: '你可以使用變數,試試輸入 {{variable}}', diff --git a/web/i18n/zh-Hant/app-log.ts b/web/i18n/zh-Hant/app-log.ts index fcedbbe53d..0d3a499fe1 100644 --- a/web/i18n/zh-Hant/app-log.ts +++ b/web/i18n/zh-Hant/app-log.ts @@ -71,7 +71,7 @@ const translation = { annotated: '已標註改進({{count}} 項)', not_annotated: '未標註', }, - sortBy: '排序方式:', + sortBy: '排序:', descending: '降序', ascending: '升序', }, diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index c43b2ee308..e5f997daff 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -92,6 +92,7 @@ const translation = { advancedUserDescription: '具有記憶體功能的多輪複雜對話任務的工作流程編排。', chooseAppType: '選擇 App Type', completionShortDescription: '用於文本生成任務的 AI 助手', + dropDSLToCreateApp: '將 DSL 檔案拖放到此處以創建應用程式', }, editApp: '編輯資訊', editAppTitle: '編輯應用資訊', @@ -135,6 +136,14 @@ const translation = { notConfigured: '配置提供商以啟用追蹤', moreProvider: '更多提供商', }, + arize: { + title: 'Arize', + description: '企業級LLM可觀測性、線上與離線評估、監控和實驗平台,基於OpenTelemetry構建,專為LLM和代理驅動的應用程式設計。', + }, + phoenix: { + title: 'Phoenix', + description: '開源且基於OpenTelemetry的可觀測性、評估、提示工程和實驗平台,適用於您的LLM工作流程和代理。', + }, langsmith: { title: 'LangSmith', description: '一個全方位的開發者平台,用於 LLM 驅動的應用程式生命週期的每個步驟。', @@ -162,6 +171,7 @@ const translation = { title: '編織', description: 'Weave 是一個開源平台,用於評估、測試和監控大型語言模型應用程序。', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: '是否使用 web app 圖示在 Explore 中取代 🤖', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 296fae9e7e..841e5b7b3e 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -58,6 +58,8 @@ const translation = { downloadSuccess: '下載完成。', downloadFailed: '下載失敗。請稍後再試。', format: '格式', + deSelectAll: '全不選', + selectAll: '全選', }, placeholder: { input: '請輸入', @@ -467,7 +469,6 @@ const translation = { apiBasedExtension: { title: 'API 擴充套件提供了一個集中式的 API 管理,在此統一新增 API 配置後,方便在 Dify 上的各類應用中直接使用。', link: '瞭解如何開發您自己的 API 擴充套件。', - linkUrl: 'https://docs.dify.ai/zh-hans/guides/tools/extensions/api-based/api-based-extension', add: '新增 API 擴充套件', selector: { title: 'API 擴充套件', diff --git a/web/i18n/zh-Hant/dataset-creation.ts b/web/i18n/zh-Hant/dataset-creation.ts index 8955deadc8..fca1ff651e 100644 --- a/web/i18n/zh-Hant/dataset-creation.ts +++ b/web/i18n/zh-Hant/dataset-creation.ts @@ -61,7 +61,6 @@ const translation = { fireCrawlNotConfiguredDescription: '使用 API 金鑰配置 Firecrawl 以使用它。', limit: '限制', crawlSubPage: '抓取子頁面', - firecrawlDocLink: 'https://docs.dify.ai/en/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', preview: '預覽', configure: '配置', excludePaths: '排除路徑', @@ -87,7 +86,6 @@ const translation = { configureFirecrawl: '配置 Firecrawl', configureWatercrawl: '配置水爬行', watercrawlTitle: '使用 Watercrawl 提取網頁內容', - watercrawlDocLink: 'https://docs.dify.ai/zh-TW/guides/knowledge-base/create-knowledge-and-upload-documents/import-content-data/sync-from-website', waterCrawlNotConfiguredDescription: '配置 Watercrawl 並使用 API 金鑰來使用它。', configureJinaReader: '配置 Jina Reader', waterCrawlNotConfigured: 'Watercrawl 尚未配置', diff --git a/web/i18n/zh-Hant/dataset-documents.ts b/web/i18n/zh-Hant/dataset-documents.ts index 60b1df80f3..104777d0f6 100644 --- a/web/i18n/zh-Hant/dataset-documents.ts +++ b/web/i18n/zh-Hant/dataset-documents.ts @@ -28,6 +28,8 @@ const translation = { delete: '刪除', enableWarning: '歸檔的檔案無法啟用', sync: '同步', + resume: '恢復', + pause: '暫停', }, index: { enable: '啟用中', @@ -388,6 +390,8 @@ const translation = { searchResults_zero: '結果', parentChunks_other: '父塊', newChildChunk: '新兒童塊', + keywordEmpty: '關鍵字不能為空', + keywordDuplicate: '關鍵字已經存在', }, } diff --git a/web/i18n/zh-Hant/dataset.ts b/web/i18n/zh-Hant/dataset.ts index 0a6b015155..676c89e185 100644 --- a/web/i18n/zh-Hant/dataset.ts +++ b/web/i18n/zh-Hant/dataset.ts @@ -179,6 +179,7 @@ const translation = { checkName: { empty: '元數據名稱不能為空', invalid: '元數據名稱只能包含小寫字母、數字和底線,並且必須以小寫字母開頭', + tooLong: '元數據名稱不能超過 {{max}} 個字符', }, batchEditMetadata: { applyToAllSelectDocumentTip: '自動為所有選定文檔創建上述所有編輯和新元數據,否則編輯元數據將僅適用於具有該元數據的文檔。', diff --git a/web/i18n/zh-Hant/plugin.ts b/web/i18n/zh-Hant/plugin.ts index 4d4e6acf2e..6b4c76495d 100644 --- a/web/i18n/zh-Hant/plugin.ts +++ b/web/i18n/zh-Hant/plugin.ts @@ -20,8 +20,8 @@ const translation = { github: '從 GitHub 安裝', marketplace: '從 Marketplace 安裝', }, - noInstalled: '未安裝外掛程式', - notFound: '未找到外掛程式', + noInstalled: '未安裝插件', + notFound: '未找到插件', }, source: { marketplace: '市場', @@ -31,12 +31,12 @@ const translation = { detailPanel: { categoryTip: { marketplace: '從 Marketplace 安裝', - debugging: '調試外掛程式', + debugging: '調試插件', github: '從 Github 安裝', - local: '本地外掛程式', + local: '本地插件', }, operation: { - info: '外掛程式資訊', + info: '插件資訊', detail: '詳', remove: '刪除', install: '安裝', @@ -45,7 +45,7 @@ const translation = { checkUpdate: '檢查更新', }, toolSelector: { - uninstalledContent: '此外掛程式是從 local/GitHub 儲存庫安裝的。請在安裝後使用。', + uninstalledContent: '此插件是從 local/GitHub 儲存庫安裝的。請在安裝後使用。', descriptionLabel: '工具描述', params: '推理配置', paramsTip2: '當 \'Automatic\' 關閉時,使用預設值。', @@ -56,9 +56,9 @@ const translation = { uninstalledTitle: '未安裝工具', auto: '自動', title: '添加工具', - unsupportedContent: '已安裝的外掛程式版本不提供此作。', + unsupportedContent: '已安裝的插件版本不提供此作。', settings: '用戶設置', - uninstalledLink: '在外掛程式中管理', + uninstalledLink: '在插件中管理', empty: '點擊 『+』 按鈕添加工具。您可以新增多個工具。', unsupportedContent2: '按兩下以切換版本。', paramsTip1: '控制 LLM 推理參數。', @@ -69,14 +69,14 @@ const translation = { strategyNum: '{{num}}{{策略}}包括', endpoints: '端點', endpointDisableTip: '禁用端點', - endpointsTip: '此外掛程式通過終端節點提供特定功能,您可以為當前工作區配置多個終端節點集。', + endpointsTip: '此插件通過終端節點提供特定功能,您可以為當前工作區配置多個終端節點集。', modelNum: '{{num}}包含的型號', endpointsEmpty: '按兩下「+」按鈕添加端點', endpointDisableContent: '您想禁用 {{name}} 嗎?', configureApp: '配置 App', endpointDeleteContent: '您想刪除 {{name}} 嗎?', configureTool: '配置工具', - endpointModalDesc: '配置后,即可使用外掛程式通過 API 端點提供的功能。', + endpointModalDesc: '配置后,即可使用插件通過 API 端點提供的功能。', disabled: '禁用', serviceOk: '服務正常', endpointDeleteTip: '刪除端點', @@ -89,26 +89,26 @@ const translation = { title: '調試', }, privilege: { - whoCanDebug: '誰可以調試外掛程式?', - whoCanInstall: '誰可以安裝和管理外掛程式?', + whoCanDebug: '誰可以調試插件?', + whoCanInstall: '誰可以安裝和管理插件?', noone: '沒人', - title: '外掛程式首選項', + title: '插件首選項', everyone: '每個人 都', admins: '管理員', }, pluginInfoModal: { repository: '存儲庫', release: '釋放', - title: '外掛程式資訊', + title: '插件資訊', packageName: '包', }, action: { - deleteContentRight: '外掛程式?', + deleteContentRight: '插件?', deleteContentLeft: '是否要刪除', - usedInApps: '此外掛程式正在 {{num}} 個應用程式中使用。', - pluginInfo: '外掛程式資訊', + usedInApps: '此插件正在 {{num}} 個應用程式中使用。', + pluginInfo: '插件資訊', checkForUpdates: '檢查更新', - delete: '刪除外掛程式', + delete: '刪除插件', }, installModal: { labels: { @@ -116,27 +116,28 @@ const translation = { version: '版本', package: '包', }, - readyToInstallPackage: '即將安裝以下外掛程式', + readyToInstallPackage: '即將安裝以下插件', back: '返回', installFailed: '安裝失敗', - readyToInstallPackages: '即將安裝以下 {{num}} 個外掛程式', + readyToInstallPackages: '即將安裝以下 {{num}} 個插件', next: '下一個', - dropPluginToInstall: '將外掛程式包拖放到此處進行安裝', - pluginLoadError: '外掛程式載入錯誤', + dropPluginToInstall: '將插件包拖放到此處進行安裝', + pluginLoadError: '插件載入錯誤', installedSuccessfully: '安裝成功', uploadFailed: '上傳失敗', - installFailedDesc: '外掛程式安裝失敗。', - fromTrustSource: '請確保您只從<trustSource>受信任的來源</trustSource>安裝外掛程式。', - pluginLoadErrorDesc: '此外掛程式將不會被安裝', + installFailedDesc: '插件安裝失敗。', + fromTrustSource: '請確保您只從<trustSource>受信任的來源</trustSource>安裝插件。', + pluginLoadErrorDesc: '此插件將不會被安裝', installComplete: '安裝完成', install: '安裝', - installedSuccessfullyDesc: '外掛程式已成功安裝。', + installedSuccessfullyDesc: '插件已成功安裝。', close: '關閉', uploadingPackage: '正在上傳 {{packageName}}...', - readyToInstall: '即將安裝以下外掛程式', + readyToInstall: '即將安裝以下插件', cancel: '取消', - installPlugin: '安裝外掛程式', + installPlugin: '安裝插件', installing: '安裝。。。', + installWarning: '此插件不允許安裝。', }, installFromGitHub: { gitHubRepo: 'GitHub 儲存庫', @@ -145,18 +146,18 @@ const translation = { uploadFailed: '上傳失敗', selectVersion: '選擇版本', selectVersionPlaceholder: '請選擇一個版本', - updatePlugin: '從 GitHub 更新外掛程式', - installPlugin: '從 GitHub 安裝外掛程式', + updatePlugin: '從 GitHub 更新插件', + installPlugin: '從 GitHub 安裝插件', installedSuccessfully: '安裝成功', selectPackage: '選擇套餐', - installNote: '請確保您只從受信任的來源安裝外掛程式。', + installNote: '請確保您只從受信任的來源安裝插件。', }, upgrade: { close: '關閉', - title: '安裝外掛程式', + title: '安裝插件', upgrade: '安裝', upgrading: '安裝。。。', - description: '即將安裝以下外掛程式', + description: '即將安裝以下插件', usedInApps: '用於 {{num}} 個應用', successfulTitle: '安裝成功', }, @@ -173,11 +174,11 @@ const translation = { mostPopular: '最受歡迎', }, discover: '發現', - noPluginFound: '未找到外掛程式', + noPluginFound: '未找到插件', empower: '為您的 AI 開發提供支援', moreFrom: '來自 Marketplace 的更多內容', and: '和', - sortBy: '黑城', + sortBy: '排序方式', viewMore: '查看更多', difyMarketplace: 'Dify 市場', pluginsResult: '{{num}} 個結果', @@ -186,20 +187,20 @@ const translation = { }, task: { installingWithError: '安裝 {{installingLength}} 個插件,{{successLength}} 成功,{{errorLength}} 失敗', - installedError: '{{errorLength}} 個外掛程式安裝失敗', - installError: '{{errorLength}} 個外掛程式安裝失敗,點擊查看', + installedError: '{{errorLength}} 個插件安裝失敗', + installError: '{{errorLength}} 個插件安裝失敗,點擊查看', installingWithSuccess: '安裝 {{installingLength}} 個插件,{{successLength}} 成功。', clearAll: '全部清除', - installing: '安裝 {{installingLength}} 個外掛程式,0 個完成。', + installing: '安裝 {{installingLength}} 個插件,0 個完成。', }, - requestAPlugin: '申请外掛程式', - submitPlugin: '提交外掛程式', + requestAPlugin: '申请插件', + publishPlugins: '發佈插件', findMoreInMarketplace: '在 Marketplace 中查找更多內容', - installPlugin: '安裝外掛程式', + installPlugin: '安裝插件', search: '搜索', allCategories: '全部分類', from: '從', - searchPlugins: '搜索外掛程式', + searchPlugins: '搜索插件', searchTools: '搜尋工具...', installAction: '安裝', installFrom: '安裝起始位置', diff --git a/web/i18n/zh-Hant/time.ts b/web/i18n/zh-Hant/time.ts index 3be2511fcc..ddb402c422 100644 --- a/web/i18n/zh-Hant/time.ts +++ b/web/i18n/zh-Hant/time.ts @@ -1,12 +1,12 @@ const translation = { daysInWeek: { - Tue: '星期二', - Wed: '星期三', - Fri: '自由', - Mon: '懷念', - Sun: '太陽', - Sat: '星期六', - Thu: '星期四', + Sun: '日', + Mon: '一', + Tue: '二', + Wed: '三', + Thu: '四', + Fri: '五', + Sat: '六', }, months: { January: '一月', diff --git a/web/i18n/zh-Hant/tools.ts b/web/i18n/zh-Hant/tools.ts index 6e5a95f2a5..93c3fda5c4 100644 --- a/web/i18n/zh-Hant/tools.ts +++ b/web/i18n/zh-Hant/tools.ts @@ -142,16 +142,92 @@ const translation = { added: '添加', manageInTools: '在工具中管理', category: '類別', - emptyTitle: '沒有可用的工作流程工具', - emptyTip: '轉到“工作流 - >發佈為工具”', - emptyTipCustom: '創建自訂工具', - emptyTitleCustom: '沒有可用的自訂工具', + custom: { + title: '沒有可用的自訂工具', + tip: '創建一個自訂工具', + }, + workflow: { + title: '沒有可用的工作流程工具', + tip: '在 Studio 中將工作流程發佈為工具', + }, + mcp: { + title: '沒有可用的 MCP 工具', + tip: '新增一個 MCP 伺服器', + }, + agent: { + title: '沒有可用的代理策略', + }, }, customToolTip: '瞭解有關 Dify 自訂工具的更多資訊', toolNameUsageTip: '用於代理推理和提示的工具調用名稱', openInStudio: '在 Studio 中打開', noTools: '未找到工具', copyToolName: '複製名稱', + mcp: { + create: { + cardTitle: '新增 MCP 伺服器 (HTTP)', + cardLink: '了解更多關於 MCP 伺服器整合', + }, + noConfigured: '未配置的伺服器', + updateTime: '已更新', + toolsCount: '{{count}} 個工具', + noTools: '沒有可用的工具', + modal: { + title: '新增 MCP 伺服器 (HTTP)', + editTitle: '編輯 MCP 伺服器 (HTTP)', + name: '名稱與圖示', + namePlaceholder: '為您的 MCP 伺服器命名', + serverUrl: '伺服器 URL', + serverUrlPlaceholder: '伺服器端點的 URL', + serverUrlWarning: '更新伺服器地址可能會干擾依賴於此伺服器的應用程式', + serverIdentifier: '伺服器識別碼', + serverIdentifierTip: '在工作區內 MCP 伺服器的唯一識別碼。僅限小寫字母、數字、底線和連字符。最多 24 個字元。', + serverIdentifierPlaceholder: '唯一識別碼,例如:my-mcp-server', + serverIdentifierWarning: '更改 ID 之後,現有應用程式將無法識別伺服器', + cancel: '取消', + save: '儲存', + confirm: '新增並授權', + }, + delete: '刪除 MCP 伺服器', + deleteConfirmTitle: '您確定要刪除 {{mcp}} 嗎?', + operation: { + edit: '編輯', + remove: '移除', + }, + authorize: '授權', + authorizing: '正在授權...', + authorizingRequired: '需要授權', + authorizeTip: '授權後,這裡將顯示工具。', + update: '更新', + updating: '更新中', + gettingTools: '獲取工具...', + updateTools: '更新工具...', + toolsEmpty: '工具未加載', + getTools: '獲取工具', + toolUpdateConfirmTitle: '更新工具列表', + toolUpdateConfirmContent: '更新工具列表可能會影響現有應用程式。您要繼續嗎?', + toolsNum: '{{count}} 個工具包含', + onlyTool: '包含 1 個工具', + identifier: '伺服器識別碼 (點擊複製)', + server: { + title: 'MCP 伺服器', + url: '伺服器 URL', + reGen: '您想要重新生成伺服器 URL 嗎?', + addDescription: '新增描述', + edit: '編輯描述', + modal: { + addTitle: '新增描述以啟用 MCP 伺服器', + editTitle: '編輯描述', + description: '描述', + descriptionPlaceholder: '說明此工具的用途及如何被 LLM 使用', + parameters: '參數', + parametersTip: '為每個參數添加描述,以幫助 LLM 理解其目的和約束。', + parametersPlaceholder: '參數的目的和約束', + confirm: '啟用 MCP 伺服器', + }, + publishTip: '應用程式尚未發布。請先發布應用程式。', + }, + }, } export default translation diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 79f164f0f0..1d29d2f5ab 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -129,6 +129,8 @@ const translation = { value: '值', valuePlaceholder: '環境值', secretTip: '用於定義敏感信息或數據,DSL 設置配置為防止洩露。', + description: '描述', + descriptionPlaceholder: '描述此變數', }, export: { title: '導出機密環境變數?', @@ -231,7 +233,7 @@ const translation = { 'noResult': '未找到匹配項', 'searchTool': '搜索工具', 'agent': '代理策略', - 'plugin': '外掛程式', + 'plugin': '插件', }, blocks: { 'start': '開始', @@ -303,6 +305,8 @@ const translation = { change: '更改', optional: '(選擇性)', moveToThisNode: '定位至此節點', + minimize: '退出全螢幕', + maximize: '最大化畫布', }, nodes: { common: { @@ -360,6 +364,7 @@ const translation = { ms: '毫秒', retries: '{{num}}重試', }, + typeSwitch: {}, }, start: { required: '必填', @@ -535,6 +540,10 @@ const translation = { placeholder: '在此處粘貼 cURL 字串', title: '從 cURL 導入', }, + verifySSL: { + title: '驗證 SSL 證書', + warningTooltip: '不建議在生產環境中禁用SSL驗證。這僅應用於開發或測試,因為這樣會使連接容易受到中間人攻擊等安全威脅的威脅。', + }, }, code: { inputVars: '輸入變量', @@ -542,6 +551,7 @@ const translation = { advancedDependencies: '高級依賴', advancedDependenciesTip: '在這裡添加一些預加載需要消耗較多時間或非默認內置的依賴包', searchDependencies: '搜索依賴', + syncFunctionSignature: '同步函數簽名至代碼', }, templateTransform: { inputVars: '輸入變量', @@ -667,6 +677,7 @@ const translation = { inputVars: '輸入變量', outputVars: { className: '分類名稱', + usage: '模型用量信息', }, class: '分類', classNamePlaceholder: '輸入你的分類名稱', @@ -680,6 +691,11 @@ const translation = { }, parameterExtractor: { inputVar: '輸入變量', + outputVars: { + isSuccess: '是否成功。成功時值為 1,失敗時值為 0。', + errorReason: '錯誤原因', + usage: '模型用量信息', + }, extractParameters: '提取參數', importFromTool: '從工具導入', addExtractParameter: '添加提取參數', @@ -699,8 +715,6 @@ const translation = { advancedSetting: '高級設置', reasoningMode: '推理模式', reasoningModeTip: '你可以根據模型對於 Function calling 或 Prompt 的指令響應能力選擇合適的推理模式', - isSuccess: '是否成功。成功時值為 1,失敗時值為 0。', - errorReason: '錯誤原因', }, iteration: { deleteTitle: '刪除迭代節點?', @@ -789,13 +803,13 @@ const translation = { }, modelNotInMarketplace: { title: '未安裝模型', - manageInPlugins: '在外掛程式中管理', + manageInPlugins: '在插件中管理', desc: '此模型是從 Local 或 GitHub 儲存庫安裝的。請在安裝後使用。', }, modelNotSupport: { title: '不支援的型號', - desc: '已安裝的外掛程式版本不提供此模型。', - descForVersionSwitch: '已安裝的外掛程式版本不提供此模型。按兩下以切換版本。', + desc: '已安裝的插件版本不提供此模型。', + descForVersionSwitch: '已安裝的插件版本不提供此模型。按兩下以切換版本。', }, modelSelectorTooltips: { deprecated: '此模型已棄用', @@ -815,18 +829,18 @@ const translation = { strategyNotSelected: '未選擇策略', }, installPlugin: { - title: '安裝外掛程式', + title: '安裝插件', changelog: '更新日誌', cancel: '取消', - desc: '即將安裝以下外掛程式', + desc: '即將安裝以下插件', install: '安裝', }, - pluginNotFoundDesc: '此外掛程式是從 GitHub 安裝的。請前往外掛程式 重新安裝', + pluginNotFoundDesc: '此插件是從 GitHub 安裝的。請前往插件 重新安裝', modelNotSelected: '未選擇模型', tools: '工具', - strategyNotFoundDesc: '已安裝的外掛程式版本不提供此策略。', - pluginNotInstalledDesc: '此外掛程式是從 GitHub 安裝的。請前往外掛程式 重新安裝', - strategyNotFoundDescAndSwitchVersion: '已安裝的外掛程式版本不提供此策略。按兩下以切換版本。', + strategyNotFoundDesc: '已安裝的插件版本不提供此策略。', + pluginNotInstalledDesc: '此插件是從 GitHub 安裝的。請前往插件 重新安裝', + strategyNotFoundDescAndSwitchVersion: '已安裝的插件版本不提供此策略。按兩下以切換版本。', strategyNotInstallTooltip: '{{strategy}} 未安裝', toolNotAuthorizedTooltip: '{{工具}}未授權', unsupportedStrategy: '不支援的策略', @@ -838,8 +852,8 @@ const translation = { toolbox: '工具箱', configureModel: '配置模型', learnMore: '瞭解更多資訊', - linkToPlugin: '連結到外掛程式', - pluginNotInstalled: '此外掛程式未安裝', + linkToPlugin: '連結到插件', + pluginNotInstalled: '此插件未安裝', notAuthorized: '未授權', }, loop: { @@ -917,6 +931,35 @@ const translation = { releaseNotesPlaceholder: '描述發生了什麼變化', defaultName: '未命名版本', }, + debug: { + noData: { + runThisNode: '運行此節點', + description: '上次運行的結果將顯示在這裡', + }, + variableInspect: { + trigger: { + cached: '查看緩存的變量', + stop: '停止跑步', + clear: '清晰', + running: '快取運行狀態', + normal: '變數檢查', + }, + emptyLink: '了解更多', + view: '查看日誌', + clearAll: '重置所有', + envNode: '環境', + title: '變數檢查', + clearNode: '清除快取變數', + systemNode: '系統', + reset: '重置為上次運行值', + chatNode: '對話', + edited: '編輯的', + emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。', + resetConversationVar: '將對話變數重置為默認值', + }, + settingsTab: '設定', + lastRunTab: '最後一次運行', + }, } export default translation diff --git a/web/models/app.ts b/web/models/app.ts index b10ced703a..5798670426 100644 --- a/web/models/app.ts +++ b/web/models/app.ts @@ -1,9 +1,9 @@ -import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' +import type { AliyunConfig, LangFuseConfig, LangSmithConfig, OpikConfig, PhoenixConfig, TracingProvider, WeaveConfig } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' import type { App, AppTemplate, SiteConfig } from '@/types/app' import type { Dependency } from '@/app/components/plugins/types' /* export type App = { - id: string + id: strin name: string description: string mode: AppMode @@ -166,5 +166,5 @@ export type TracingStatus = { export type TracingConfig = { tracing_provider: TracingProvider - tracing_config: LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig + tracing_config: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig } diff --git a/web/models/log.ts b/web/models/log.ts index 9886fe215b..31bcaa727c 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -278,7 +278,6 @@ export type WorkflowLogsRequest = { export type WorkflowRunDetailResponse = { id: string - sequence_number: number version: string graph: { nodes: Node[] diff --git a/web/package.json b/web/package.json index affbef9382..9099d3ed36 100644 --- a/web/package.json +++ b/web/package.json @@ -1,10 +1,22 @@ { "name": "dify-web", - "version": "1.4.1", + "version": "1.6.0", "private": true, "engines": { "node": ">=v22.11.0" }, + "browserslist": [ + "last 1 Chrome version", + "last 1 Firefox version", + "last 1 Edge version", + "last 1 Safari version", + "iOS >=15", + "Android >= 10", + "and_chr >= 126", + "and_ff >= 137", + "and_uc >= 15.5", + "and_qq >= 14.9" + ], "scripts": { "dev": "cross-env NODE_OPTIONS='--inspect' next dev", "build": "next build", @@ -46,7 +58,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@monaco-editor/react": "^4.6.0", - "@next/mdx": "15.2.3", + "@next/mdx": "~15.3.5", "@octokit/core": "^6.1.2", "@octokit/request-error": "^6.1.5", "@remixicon/react": "^4.5.0", @@ -91,14 +103,14 @@ "mime": "^4.0.4", "mitt": "^3.0.1", "negotiator": "^0.6.3", - "next": "15.2.3", + "next": "~15.3.5", "next-themes": "^0.4.3", "pinyin-pro": "^3.25.0", "qrcode.react": "^4.2.0", "qs": "^6.13.0", - "react": "19.0.0", + "react": "~19.1.0", "react-18-input-autosize": "^3.0.0", - "react-dom": "19.0.0", + "react-dom": "~19.1.0", "react-easy-crop": "^5.1.0", "react-error-boundary": "^4.1.2", "react-headless-pagination": "^1.1.6", @@ -132,6 +144,7 @@ "sortablejs": "^1.15.0", "swr": "^2.3.0", "tailwind-merge": "^2.5.4", + "tldts": "^7.0.9", "use-context-selector": "^2.0.0", "uuid": "^10.0.0", "zod": "^3.23.8", @@ -146,7 +159,7 @@ "@eslint/js": "^9.20.0", "@faker-js/faker": "^9.0.3", "@happy-dom/jest-environment": "^17.4.4", - "@next/eslint-plugin-next": "^15.2.3", + "@next/eslint-plugin-next": "~15.3.5", "@rgrove/parse-xml": "^4.1.0", "@storybook/addon-essentials": "8.5.0", "@storybook/addon-interactions": "8.5.0", @@ -168,8 +181,8 @@ "@types/negotiator": "^0.6.3", "@types/node": "18.15.0", "@types/qs": "^6.9.16", - "@types/react": "19.0.11", - "@types/react-dom": "19.0.4", + "@types/react": "~19.1.8", + "@types/react-dom": "~19.1.6", "@types/react-slider": "^1.3.6", "@types/react-syntax-highlighter": "^15.5.13", "@types/react-window": "^1.8.8", @@ -183,7 +196,7 @@ "code-inspector-plugin": "^0.18.1", "cross-env": "^7.0.3", "eslint": "^9.20.1", - "eslint-config-next": "^15.0.0", + "eslint-config-next": "~15.3.5", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-sonarjs": "^3.0.2", @@ -199,13 +212,13 @@ "storybook": "8.5.0", "tailwindcss": "^3.4.14", "ts-node": "^10.9.2", - "typescript": "4.9.5", - "typescript-eslint": "^8.23.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.36.0", "uglify-js": "^3.19.3" }, "resolutions": { - "@types/react": "~18.2.0", - "@types/react-dom": "~18.2.0", + "@types/react": "~19.1.8", + "@types/react-dom": "~19.1.6", "string-width": "4.2.3" }, "lint-staged": { @@ -223,7 +236,11 @@ }, "pnpm": { "overrides": { - "esbuild@<0.25.0": "0.25.0" + "esbuild@<0.25.0": "0.25.0", + "pbkdf2@<3.1.3": "3.1.3", + "vite@<6.2.7": "6.2.7", + "prismjs@<1.30.0": "1.30.0", + "brace-expansion@<2.0.2": "2.0.2" } } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index fce3b6581b..746b2b3e72 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -5,10 +5,14 @@ settings: excludeLinksFromLockfile: false overrides: - '@types/react': ~18.2.0 - '@types/react-dom': ~18.2.0 + '@types/react': ~19.1.8 + '@types/react-dom': ~19.1.6 string-width: 4.2.3 esbuild@<0.25.0: 0.25.0 + pbkdf2@<3.1.3: 3.1.3 + vite@<6.2.7: 6.2.7 + prismjs@<1.30.0: 1.30.0 + brace-expansion@<2.0.2: 2.0.2 importers: @@ -28,19 +32,19 @@ importers: version: 1.2.8(eslint@9.24.0(jiti@1.21.7)) '@floating-ui/react': specifier: ^0.26.25 - version: 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@formatjs/intl-localematcher': specifier: ^0.5.6 version: 0.5.10 '@headlessui/react': specifier: ^2.2.0 - version: 2.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@heroicons/react': specifier: ^2.0.16 - version: 2.2.0(react@19.0.0) + version: 2.2.0(react@19.1.0) '@hookform/resolvers': specifier: ^3.9.0 - version: 3.10.0(react-hook-form@7.55.0(react@19.0.0)) + version: 3.10.0(react-hook-form@7.55.0(react@19.1.0)) '@lexical/code': specifier: ^0.30.0 version: 0.30.0 @@ -52,7 +56,7 @@ importers: version: 0.30.0 '@lexical/react': specifier: ^0.30.0 - version: 0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24) + version: 0.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.24) '@lexical/selection': specifier: ^0.30.0 version: 0.30.0 @@ -67,13 +71,13 @@ importers: version: 3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) '@mdx-js/react': specifier: ^3.1.0 - version: 3.1.0(@types/react@18.2.79)(react@19.0.0) + version: 3.1.0(@types/react@19.1.8)(react@19.1.0) '@monaco-editor/react': specifier: ^4.6.0 - version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@next/mdx': - specifier: 15.2.3 - version: 15.2.3(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@18.2.79)(react@19.0.0)) + specifier: ~15.3.5 + version: 15.3.5(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)) '@octokit/core': specifier: ^6.1.2 version: 6.1.5 @@ -82,10 +86,10 @@ importers: version: 6.1.8 '@remixicon/react': specifier: ^4.5.0 - version: 4.6.0(react@19.0.0) + version: 4.6.0(react@19.1.0) '@sentry/react': specifier: ^8.54.0 - version: 8.55.0(react@19.0.0) + version: 8.55.0(react@19.1.0) '@sentry/utils': specifier: ^8.54.0 version: 8.55.0 @@ -94,22 +98,22 @@ importers: version: 3.2.4 '@tailwindcss/typography': specifier: ^0.5.15 - version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))) + version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3))) '@tanstack/react-form': specifier: ^1.3.3 - version: 1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-query': specifier: ^5.60.5 - version: 5.72.2(react@19.0.0) + version: 5.72.2(react@19.1.0) '@tanstack/react-query-devtools': specifier: ^5.60.5 - version: 5.72.2(@tanstack/react-query@5.72.2(react@19.0.0))(react@19.0.0) + version: 5.72.2(@tanstack/react-query@5.72.2(react@19.1.0))(react@19.1.0) abcjs: specifier: ^6.4.4 version: 6.4.4 ahooks: specifier: ^3.8.4 - version: 3.8.4(react@19.0.0) + version: 3.8.4(react@19.1.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.1 @@ -139,7 +143,7 @@ importers: version: 5.6.0 echarts-for-react: specifier: ^3.0.2 - version: 3.0.2(echarts@5.6.0)(react@19.0.0) + version: 3.0.2(echarts@5.6.0)(react@19.1.0) elkjs: specifier: ^0.9.3 version: 0.9.3 @@ -207,86 +211,86 @@ importers: specifier: ^0.6.3 version: 0.6.4 next: - specifier: 15.2.3 - version: 15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3) + specifier: ~15.3.5 + version: 15.3.5(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3) next-themes: specifier: ^0.4.3 - version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) pinyin-pro: specifier: ^3.25.0 version: 3.26.0 qrcode.react: specifier: ^4.2.0 - version: 4.2.0(react@19.0.0) + version: 4.2.0(react@19.1.0) qs: specifier: ^6.13.0 version: 6.14.0 react: - specifier: 19.0.0 - version: 19.0.0 + specifier: ~19.1.0 + version: 19.1.0 react-18-input-autosize: specifier: ^3.0.0 - version: 3.0.0(react@19.0.0) + version: 3.0.0(react@19.1.0) react-dom: - specifier: 19.0.0 - version: 19.0.0(react@19.0.0) + specifier: ~19.1.0 + version: 19.1.0(react@19.1.0) react-easy-crop: specifier: ^5.1.0 - version: 5.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 5.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-error-boundary: specifier: ^4.1.2 - version: 4.1.2(react@19.0.0) + version: 4.1.2(react@19.1.0) react-headless-pagination: specifier: ^1.1.6 - version: 1.1.6(react@19.0.0) + version: 1.1.6(react@19.1.0) react-hook-form: specifier: ^7.53.1 - version: 7.55.0(react@19.0.0) + version: 7.55.0(react@19.1.0) react-hotkeys-hook: specifier: ^4.6.1 - version: 4.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 4.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-i18next: specifier: ^15.1.0 - version: 15.4.1(i18next@23.16.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.4.1(i18next@23.16.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-infinite-scroll-component: specifier: ^6.1.0 - version: 6.1.0(react@19.0.0) + version: 6.1.0(react@19.1.0) react-markdown: specifier: ^9.0.1 - version: 9.1.0(@types/react@18.2.79)(react@19.0.0) + version: 9.1.0(@types/react@19.1.8)(react@19.1.0) react-multi-email: specifier: ^1.0.25 - version: 1.0.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.0.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-papaparse: specifier: ^4.4.0 version: 4.4.0 react-pdf-highlighter: specifier: ^8.0.0-rc.0 - version: 8.0.0-rc.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 8.0.0-rc.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-slider: specifier: ^2.0.6 - version: 2.0.6(react@19.0.0) + version: 2.0.6(react@19.1.0) react-sortablejs: specifier: ^6.1.4 - version: 6.1.4(@types/sortablejs@1.15.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sortablejs@1.15.6) + version: 6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6) react-syntax-highlighter: specifier: ^15.6.1 - version: 15.6.1(react@19.0.0) + version: 15.6.1(react@19.1.0) react-textarea-autosize: specifier: ^8.5.8 - version: 8.5.9(@types/react@18.2.79)(react@19.0.0) + version: 8.5.9(@types/react@19.1.8)(react@19.1.0) react-tooltip: specifier: 5.8.3 - version: 5.8.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 5.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-window: specifier: ^1.8.10 - version: 1.8.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.8.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-window-infinite-loader: specifier: ^1.0.9 - version: 1.0.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 1.0.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) reactflow: specifier: ^11.11.3 - version: 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) recordrtc: specifier: ^5.6.2 version: 5.6.2 @@ -325,13 +329,16 @@ importers: version: 1.15.6 swr: specifier: ^2.3.0 - version: 2.3.3(react@19.0.0) + version: 2.3.3(react@19.1.0) tailwind-merge: specifier: ^2.5.4 version: 2.6.0 + tldts: + specifier: ^7.0.9 + version: 7.0.9 use-context-selector: specifier: ^2.0.0 - version: 2.0.0(react@19.0.0)(scheduler@0.23.2) + version: 2.0.0(react@19.1.0)(scheduler@0.23.2) uuid: specifier: ^10.0.0 version: 10.0.0 @@ -340,20 +347,20 @@ importers: version: 3.24.2 zundo: specifier: ^2.1.0 - version: 2.3.0(zustand@4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0)) + version: 2.3.0(zustand@4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0)) zustand: specifier: ^4.5.2 - version: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + version: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) devDependencies: '@antfu/eslint-config': specifier: ^4.1.1 - version: 4.12.0(@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5))(@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(@vue/compiler-sfc@3.5.13)(eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@1.21.7)))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) + version: 4.12.0(@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@typescript-eslint/utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@1.21.7)))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) '@chromatic-com/storybook': specifier: ^3.1.0 - version: 3.2.6(react@19.0.0)(storybook@8.5.0) + version: 3.2.6(react@19.1.0)(storybook@8.5.0) '@eslint-react/eslint-plugin': specifier: ^1.15.0 - version: 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5) + version: 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) '@eslint/eslintrc': specifier: ^3.1.0 version: 3.3.1 @@ -367,20 +374,20 @@ importers: specifier: ^17.4.4 version: 17.4.4 '@next/eslint-plugin-next': - specifier: ^15.2.3 - version: 15.3.0 + specifier: ~15.3.5 + version: 15.3.5 '@rgrove/parse-xml': specifier: ^4.1.0 version: 4.2.0 '@storybook/addon-essentials': specifier: 8.5.0 - version: 8.5.0(@types/react@18.2.79)(storybook@8.5.0) + version: 8.5.0(@types/react@19.1.8)(storybook@8.5.0) '@storybook/addon-interactions': specifier: 8.5.0 version: 8.5.0(storybook@8.5.0) '@storybook/addon-links': specifier: 8.5.0 - version: 8.5.0(react@19.0.0)(storybook@8.5.0) + version: 8.5.0(react@19.1.0)(storybook@8.5.0) '@storybook/addon-onboarding': specifier: 8.5.0 version: 8.5.0(storybook@8.5.0) @@ -389,13 +396,13 @@ importers: version: 8.5.0(storybook@8.5.0) '@storybook/blocks': specifier: 8.5.0 - version: 8.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0) + version: 8.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0) '@storybook/nextjs': specifier: 8.5.0 - version: 8.5.0(esbuild@0.25.0)(next@15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3)(storybook@8.5.0)(type-fest@4.39.1)(typescript@4.9.5)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) + version: 8.5.0(esbuild@0.25.0)(next@15.3.5(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3)(storybook@8.5.0)(type-fest@4.39.1)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) '@storybook/react': specifier: 8.5.0 - version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5) + version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3) '@storybook/test': specifier: 8.5.0 version: 8.5.0(storybook@8.5.0) @@ -407,7 +414,7 @@ importers: version: 6.6.3 '@testing-library/react': specifier: ^16.0.1 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 @@ -433,11 +440,11 @@ importers: specifier: ^6.9.16 version: 6.9.18 '@types/react': - specifier: ~18.2.0 - version: 18.2.79 + specifier: ~19.1.8 + version: 19.1.8 '@types/react-dom': - specifier: ~18.2.0 - version: 18.2.25 + specifier: ~19.1.6 + version: 19.1.6(@types/react@19.1.8) '@types/react-slider': specifier: ^1.3.6 version: 1.3.6 @@ -478,8 +485,8 @@ importers: specifier: ^9.20.1 version: 9.24.0(jiti@1.21.7) eslint-config-next: - specifier: ^15.0.0 - version: 15.3.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + specifier: ~15.3.5 + version: 15.3.5(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint-plugin-react-hooks: specifier: ^5.1.0 version: 5.2.0(eslint@9.24.0(jiti@1.21.7)) @@ -491,16 +498,16 @@ importers: version: 3.0.2(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-storybook: specifier: ^0.11.2 - version: 0.11.6(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + version: 0.11.6(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint-plugin-tailwindcss: specifier: ^3.18.0 - version: 3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))) + version: 3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3))) husky: specifier: ^9.1.6 version: 9.1.7 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) lint-staged: specifier: ^15.2.10 version: 15.5.0 @@ -521,16 +528,16 @@ importers: version: 8.5.0 tailwindcss: specifier: ^3.4.14 - version: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + version: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@18.15.0)(typescript@4.9.5) + version: 10.9.2(@types/node@18.15.0)(typescript@5.8.3) typescript: - specifier: 4.9.5 - version: 4.9.5 + specifier: ^5.8.3 + version: 5.8.3 typescript-eslint: - specifier: ^8.23.0 - version: 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + specifier: ^8.36.0 + version: 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) uglify-js: specifier: ^3.19.3 version: 3.19.3 @@ -1276,6 +1283,9 @@ packages: '@emnapi/runtime@1.4.0': resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} + '@emnapi/runtime@1.4.4': + resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/wasi-threads@1.0.1': resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} @@ -1602,6 +1612,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -1777,105 +1793,227 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2067,7 +2205,7 @@ packages: '@mdx-js/react@3.1.0': resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} peerDependencies: - '@types/react': ~18.2.0 + '@types/react': ~19.1.8 react: '>=16' '@mermaid-js/parser@0.3.0': @@ -2086,14 +2224,14 @@ packages: '@napi-rs/wasm-runtime@0.2.8': resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} - '@next/env@15.2.3': - resolution: {integrity: sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==} + '@next/env@15.3.5': + resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} - '@next/eslint-plugin-next@15.3.0': - resolution: {integrity: sha512-511UUcpWw5GWTyKfzW58U2F/bYJyjLE9e3SlnGK/zSXq7RqLlqFO8B9bitJjumLpj317fycC96KZ2RZsjGNfBw==} + '@next/eslint-plugin-next@15.3.5': + resolution: {integrity: sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w==} - '@next/mdx@15.2.3': - resolution: {integrity: sha512-rJAe5GvpTTA/i+9lQk/p321g0kXPLIuWJzUtRccW7w4l9vmOTGPPnXFjooEyYgyFcdbZxvJpSdjNq65VeQGKRQ==} + '@next/mdx@15.3.5': + resolution: {integrity: sha512-/2rRCgPKNp2ttQscU13auI+cYYACdPa80Okgi/1+NNJJeWn9yVxwGnqZc3SX30T889bZbLqcY4oUjqYGAygL4g==} peerDependencies: '@mdx-js/loader': '>=0.15.0' '@mdx-js/react': '>=0.15.0' @@ -2103,50 +2241,50 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@15.2.3': - resolution: {integrity: sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==} + '@next/swc-darwin-arm64@15.3.5': + resolution: {integrity: sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.2.3': - resolution: {integrity: sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==} + '@next/swc-darwin-x64@15.3.5': + resolution: {integrity: sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.2.3': - resolution: {integrity: sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==} + '@next/swc-linux-arm64-gnu@15.3.5': + resolution: {integrity: sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.2.3': - resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==} + '@next/swc-linux-arm64-musl@15.3.5': + resolution: {integrity: sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.2.3': - resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==} + '@next/swc-linux-x64-gnu@15.3.5': + resolution: {integrity: sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.2.3': - resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==} + '@next/swc-linux-x64-musl@15.3.5': + resolution: {integrity: sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.2.3': - resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==} + '@next/swc-win32-arm64-msvc@15.3.5': + resolution: {integrity: sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.2.3': - resolution: {integrity: sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==} + '@next/swc-win32-x64-msvc@15.3.5': + resolution: {integrity: sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2861,8 +2999,8 @@ packages: engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 - '@types/react': ~18.2.0 - '@types/react-dom': ~18.2.0 + '@types/react': ~19.1.8 + '@types/react-dom': ~19.1.6 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: @@ -3102,14 +3240,13 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - '@types/prop-types@15.7.14': - resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} - '@types/react-dom@18.2.25': - resolution: {integrity: sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==} + '@types/react-dom@19.1.6': + resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} + peerDependencies: + '@types/react': ~19.1.8 '@types/react-slider@1.3.6': resolution: {integrity: sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==} @@ -3123,8 +3260,8 @@ packages: '@types/react-window@1.8.8': resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} - '@types/react@18.2.79': - resolution: {integrity: sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==} + '@types/react@19.1.8': + resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} '@types/recordrtc@5.6.14': resolution: {integrity: sha512-Reiy1sl11xP0r6w8DW3iQjc1BgXFyNC7aDuutysIjpFoqyftbQps9xPA2FoBkfVXpJM61betgYPNt+v65zvMhA==} @@ -3173,6 +3310,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/eslint-plugin@8.36.0': + resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.36.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/parser@8.29.1': resolution: {integrity: sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3180,10 +3325,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/parser@8.36.0': + resolution: {integrity: sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.36.0': + resolution: {integrity: sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/scope-manager@8.29.1': resolution: {integrity: sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.36.0': + resolution: {integrity: sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.36.0': + resolution: {integrity: sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/type-utils@8.29.1': resolution: {integrity: sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3191,16 +3359,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/type-utils@8.36.0': + resolution: {integrity: sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/types@8.29.1': resolution: {integrity: sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.36.0': + resolution: {integrity: sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.29.1': resolution: {integrity: sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/typescript-estree@8.36.0': + resolution: {integrity: sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/utils@8.29.1': resolution: {integrity: sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3208,10 +3393,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/utils@8.36.0': + resolution: {integrity: sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/visitor-keys@8.29.1': resolution: {integrity: sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.36.0': + resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -3311,7 +3507,7 @@ packages: resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 + vite: 6.2.7 peerDependenciesMeta: msw: optional: true @@ -3726,11 +3922,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -4076,9 +4269,6 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -4134,6 +4324,9 @@ packages: create-ecdh@4.0.4: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + create-hash@1.1.3: + resolution: {integrity: sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==} + create-hash@1.2.0: resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} @@ -4462,6 +4655,10 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -4699,8 +4896,8 @@ packages: peerDependencies: eslint: ^9.5.0 - eslint-config-next@15.3.0: - resolution: {integrity: sha512-+Z3M1W9MnJjX3W4vI9CHfKlEyhTWOUHvc5dB89FyRnzPsUkJlLWZOi8+1pInuVcSztSM4MwBFB0hIHf4Rbwu4g==} + eslint-config-next@15.3.5: + resolution: {integrity: sha512-oQdvnIgP68wh2RlR3MdQpvaJ94R6qEFl+lnu8ZKxPj5fsAHrSF/HlAOZcsimLw3DT6bnEQIUdbZC2Ab6sWyptg==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 typescript: '>=3.3.1' @@ -4994,6 +5191,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.24.0: resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5383,6 +5584,9 @@ packages: has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hash-base@2.0.2: + resolution: {integrity: sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==} + hash-base@3.0.5: resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} engines: {node: '>= 0.10'} @@ -5547,6 +5751,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + image-size@1.2.1: resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} engines: {node: '>=16.x'} @@ -6565,8 +6773,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.2.3: - resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==} + next@15.3.5: + resolution: {integrity: sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -6859,8 +7067,8 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - pbkdf2@3.1.2: - resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + pbkdf2@3.1.3: + resolution: {integrity: sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==} engines: {node: '>=0.12'} pdfjs-dist@4.4.168: @@ -7042,10 +7250,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - prismjs@1.27.0: - resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} - engines: {node: '>=6'} - prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -7156,10 +7360,10 @@ packages: peerDependencies: react: ^18.3.1 - react-dom@19.0.0: - resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: - react: ^19.0.0 + react: ^19.1.0 react-draggable@4.4.6: resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} @@ -7235,7 +7439,7 @@ packages: react-markdown@9.1.0: resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} peerDependencies: - '@types/react': ~18.2.0 + '@types/react': ~19.1.8 react: '>=18' react-multi-email@1.0.25: @@ -7312,8 +7516,8 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.0.0: - resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} reactflow@11.11.4: @@ -7527,6 +7731,9 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + ripemd160@2.0.1: + resolution: {integrity: sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==} + ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} @@ -7597,8 +7804,8 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} schema-utils@3.3.0: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} @@ -7625,6 +7832,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -7657,6 +7869,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shave@5.0.4: resolution: {integrity: sha512-AnvEI1wM2rQmrwCl364LVLLhzCzSHJ7DQmdd+fHJTnNzbD2mjsUAOcxWLLYKam7Q63skwyQf2CB2TCdJ2O5c8w==} @@ -8039,9 +8255,20 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.9: + resolution: {integrity: sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==} + + tldts@7.0.9: + resolution: {integrity: sha512-/nFtBeNs9nAKIAZE1i3ssOAroci8UqRldFVw5H6RCsNZw7NzDr+Yc3Ek7Tm8XSQKMzw7NSyRSszNxCM0ENsUbg==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-buffer@1.2.1: + resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -8171,18 +8398,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.29.1: - resolution: {integrity: sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==} + typescript-eslint@8.36.0: + resolution: {integrity: sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -8367,8 +8589,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.2.6: - resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==} + vite@6.2.7: + resolution: {integrity: sha512-qg3LkeuinTrZoJHHF94coSaTfIPyBYoywp+ys4qu20oSJFbKMYoIJo0FWJT9q6Vp49l6z9IsJRbHdcGtiKbGoQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -8658,7 +8880,7 @@ packages: resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==} engines: {node: '>=12.7.0'} peerDependencies: - '@types/react': ~18.2.0 + '@types/react': ~19.1.8 immer: '>=9.0.6' react: '>=16.8' peerDependenciesMeta: @@ -8683,16 +8905,16 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@4.12.0(@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5))(@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(@vue/compiler-sfc@3.5.13)(eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@1.21.7)))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': + '@antfu/eslint-config@4.12.0(@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3))(@typescript-eslint/utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-react-hooks@5.2.0(eslint@9.24.0(jiti@1.21.7)))(eslint-plugin-react-refresh@0.4.19(eslint@9.24.0(jiti@1.21.7)))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': dependencies: '@antfu/install-pkg': 1.0.0 '@clack/prompts': 0.10.1 '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.24.0(jiti@1.21.7)) '@eslint/markdown': 6.3.0 - '@stylistic/eslint-plugin': 4.2.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@vitest/eslint-plugin': 1.1.42(@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) + '@stylistic/eslint-plugin': 4.2.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@vitest/eslint-plugin': 1.1.42(@typescript-eslint/utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) ansis: 3.17.0 cac: 6.7.14 eslint: 9.24.0(jiti@1.21.7) @@ -8701,17 +8923,17 @@ snapshots: eslint-merge-processors: 2.0.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-antfu: 3.1.1(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-command: 3.2.0(eslint@9.24.0(jiti@1.21.7)) - eslint-plugin-import-x: 4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + eslint-plugin-import-x: 4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint-plugin-jsdoc: 50.6.9(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-jsonc: 2.20.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-n: 17.17.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-perfectionist: 4.11.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + eslint-plugin-perfectionist: 4.11.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint-plugin-pnpm: 0.3.1(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-regexp: 2.7.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-toml: 0.12.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-unicorn: 58.0.0(eslint@9.24.0(jiti@1.21.7)) - eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7)) + eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-vue: 10.0.0(eslint@9.24.0(jiti@1.21.7))(vue-eslint-parser@10.1.3(eslint@9.24.0(jiti@1.21.7))) eslint-plugin-yml: 1.17.0(eslint@9.24.0(jiti@1.21.7)) eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.24.0(jiti@1.21.7)) @@ -8723,7 +8945,7 @@ snapshots: vue-eslint-parser: 10.1.3(eslint@9.24.0(jiti@1.21.7)) yaml-eslint-parser: 1.3.0 optionalDependencies: - '@eslint-react/eslint-plugin': 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5) + '@eslint-react/eslint-plugin': 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-react-refresh: 0.4.19(eslint@9.24.0(jiti@1.21.7)) transitivePeerDependencies: @@ -9561,12 +9783,12 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@chromatic-com/storybook@3.2.6(react@19.0.0)(storybook@8.5.0)': + '@chromatic-com/storybook@3.2.6(react@19.1.0)(storybook@8.5.0)': dependencies: chromatic: 11.28.0 filesize: 10.1.6 jsonfile: 6.1.0 - react-confetti: 6.4.0(react@19.0.0) + react-confetti: 6.4.0(react@19.1.0) storybook: 8.5.0 strip-ansi: 7.1.0 transitivePeerDependencies: @@ -9606,6 +9828,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.4.4': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.0.1': dependencies: tslib: 2.8.1 @@ -9789,14 +10016,19 @@ snapshots: eslint: 9.24.0(jiti@1.21.7) eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.7.0(eslint@9.24.0(jiti@1.21.7))': + dependencies: + eslint: 9.24.0(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} - '@eslint-react/ast@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/ast@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-react/eff': 1.45.0 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@4.9.5) - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.0 transitivePeerDependencies: @@ -9804,18 +10036,18 @@ snapshots: - supports-color - typescript - '@eslint-react/core@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/core@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) birecord: 0.1.1 ts-pattern: 5.7.0 transitivePeerDependencies: @@ -9825,58 +10057,58 @@ snapshots: '@eslint-react/eff@1.45.0': {} - '@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5)': + '@eslint-react/eslint-plugin@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3)': dependencies: '@eslint-react/eff': 1.45.0 - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) - eslint-plugin-react-debug: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - eslint-plugin-react-dom: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - eslint-plugin-react-hooks-extra: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - eslint-plugin-react-naming-convention: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - eslint-plugin-react-web-api: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - eslint-plugin-react-x: 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5) + eslint-plugin-react-debug: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-react-dom: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-react-hooks-extra: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-react-naming-convention: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-react-web-api: 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + eslint-plugin-react-x: 1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3) optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color - ts-api-utils - '@eslint-react/jsx@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/jsx@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) ts-pattern: 5.7.0 transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/kit@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/kit@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-react/eff': 1.45.0 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) ts-pattern: 5.7.0 - valibot: 1.0.0(typescript@4.9.5) + valibot: 1.0.0(typescript@5.8.3) transitivePeerDependencies: - eslint - supports-color - typescript - '@eslint-react/shared@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/shared@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-react/eff': 1.45.0 - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@zod/mini': 4.0.0-beta.0 picomatch: 4.0.2 ts-pattern: 5.7.0 @@ -9885,13 +10117,13 @@ snapshots: - supports-color - typescript - '@eslint-react/var@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@eslint-react/var@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.0 transitivePeerDependencies: @@ -9973,18 +10205,18 @@ snapshots: '@floating-ui/core': 1.6.9 '@floating-ui/utils': 0.2.9 - '@floating-ui/react-dom@2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react-dom@2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/dom': 1.6.13 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - '@floating-ui/react@0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@floating-ui/react@0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@floating-ui/react-dom': 2.1.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@floating-ui/utils': 0.2.9 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) tabbable: 6.2.0 '@floating-ui/utils@0.2.9': {} @@ -10002,22 +10234,22 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - '@headlessui/react@2.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@headlessui/react@2.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/focus': 3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@tanstack/react-virtual': 3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/focus': 3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/interactions': 3.24.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-virtual': 3.13.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - '@heroicons/react@2.2.0(react@19.0.0)': + '@heroicons/react@2.2.0(react@19.1.0)': dependencies: - react: 19.0.0 + react: 19.1.0 - '@hookform/resolvers@3.10.0(react-hook-form@7.55.0(react@19.0.0))': + '@hookform/resolvers@3.10.0(react-hook-form@7.55.0(react@19.1.0))': dependencies: - react-hook-form: 7.55.0(react@19.0.0) + react-hook-form: 7.55.0(react@19.1.0) '@humanfs/core@0.19.1': {} @@ -10052,76 +10284,162 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.4.0 optional: true + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.4.4 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.3': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.3': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 4.2.3 @@ -10150,7 +10468,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -10164,7 +10482,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -10344,7 +10662,7 @@ snapshots: lexical: 0.30.0 prismjs: 1.30.0 - '@lexical/devtools-core@0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@lexical/devtools-core@0.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@lexical/html': 0.30.0 '@lexical/link': 0.30.0 @@ -10352,8 +10670,8 @@ snapshots: '@lexical/table': 0.30.0 '@lexical/utils': 0.30.0 lexical: 0.30.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) '@lexical/dragon@0.30.0': dependencies: @@ -10416,9 +10734,9 @@ snapshots: '@lexical/utils': 0.30.0 lexical: 0.30.0 - '@lexical/react@0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(yjs@13.6.24)': + '@lexical/react@0.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(yjs@13.6.24)': dependencies: - '@lexical/devtools-core': 0.30.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@lexical/devtools-core': 0.30.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@lexical/dragon': 0.30.0 '@lexical/hashtag': 0.30.0 '@lexical/history': 0.30.0 @@ -10434,9 +10752,9 @@ snapshots: '@lexical/utils': 0.30.0 '@lexical/yjs': 0.30.0(yjs@13.6.24) lexical: 0.30.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - react-error-boundary: 3.1.4(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-error-boundary: 3.1.4(react@19.1.0) transitivePeerDependencies: - yjs @@ -10531,17 +10849,17 @@ snapshots: - acorn - supports-color - '@mdx-js/react@3.1.0(@types/react@18.2.79)(react@18.3.1)': + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 18.2.79 + '@types/react': 19.1.8 react: 18.3.1 - '@mdx-js/react@3.1.0(@types/react@18.2.79)(react@19.0.0)': + '@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 18.2.79 - react: 19.0.0 + '@types/react': 19.1.8 + react: 19.1.0 '@mermaid-js/parser@0.3.0': dependencies: @@ -10551,12 +10869,12 @@ snapshots: dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@monaco-editor/loader': 1.5.0 monaco-editor: 0.52.2 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) '@napi-rs/wasm-runtime@0.2.8': dependencies: @@ -10565,41 +10883,41 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true - '@next/env@15.2.3': {} + '@next/env@15.3.5': {} - '@next/eslint-plugin-next@15.3.0': + '@next/eslint-plugin-next@15.3.5': dependencies: fast-glob: 3.3.1 - '@next/mdx@15.2.3(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@18.2.79)(react@19.0.0))': + '@next/mdx@15.3.5(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0))': dependencies: source-map: 0.7.4 optionalDependencies: '@mdx-js/loader': 3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) - '@mdx-js/react': 3.1.0(@types/react@18.2.79)(react@19.0.0) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) - '@next/swc-darwin-arm64@15.2.3': + '@next/swc-darwin-arm64@15.3.5': optional: true - '@next/swc-darwin-x64@15.2.3': + '@next/swc-darwin-x64@15.3.5': optional: true - '@next/swc-linux-arm64-gnu@15.2.3': + '@next/swc-linux-arm64-gnu@15.3.5': optional: true - '@next/swc-linux-arm64-musl@15.2.3': + '@next/swc-linux-arm64-musl@15.3.5': optional: true - '@next/swc-linux-x64-gnu@15.2.3': + '@next/swc-linux-x64-gnu@15.3.5': optional: true - '@next/swc-linux-x64-musl@15.2.3': + '@next/swc-linux-x64-musl@15.3.5': optional: true - '@next/swc-win32-arm64-msvc@15.2.3': + '@next/swc-win32-arm64-msvc@15.3.5': optional: true - '@next/swc-win32-x64-msvc@15.2.3': + '@next/swc-win32-x64-msvc@15.3.5': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10740,78 +11058,78 @@ snapshots: type-fest: 4.39.1 webpack-hot-middleware: 2.26.1 - '@react-aria/focus@3.20.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/focus@3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-aria/interactions': 3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@react-types/shared': 3.28.0(react@19.0.0) + '@react-aria/interactions': 3.24.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/utils': 3.28.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-types/shared': 3.28.0(react@19.1.0) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - '@react-aria/interactions@3.24.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/interactions@3.24.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) - '@react-aria/utils': 3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@react-aria/ssr': 3.9.7(react@19.1.0) + '@react-aria/utils': 3.28.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-stately/flags': 3.1.0 - '@react-types/shared': 3.28.0(react@19.0.0) + '@react-types/shared': 3.28.0(react@19.1.0) '@swc/helpers': 0.5.17 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - '@react-aria/ssr@3.9.7(react@19.0.0)': + '@react-aria/ssr@3.9.7(react@19.1.0)': dependencies: '@swc/helpers': 0.5.17 - react: 19.0.0 + react: 19.1.0 - '@react-aria/utils@3.28.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@react-aria/utils@3.28.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@react-aria/ssr': 3.9.7(react@19.0.0) + '@react-aria/ssr': 3.9.7(react@19.1.0) '@react-stately/flags': 3.1.0 - '@react-stately/utils': 3.10.5(react@19.0.0) - '@react-types/shared': 3.28.0(react@19.0.0) + '@react-stately/utils': 3.10.5(react@19.1.0) + '@react-types/shared': 3.28.0(react@19.1.0) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) '@react-stately/flags@3.1.0': dependencies: '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.5(react@19.0.0)': + '@react-stately/utils@3.10.5(react@19.1.0)': dependencies: '@swc/helpers': 0.5.17 - react: 19.0.0 + react: 19.1.0 - '@react-types/shared@3.28.0(react@19.0.0)': + '@react-types/shared@3.28.0(react@19.1.0)': dependencies: - react: 19.0.0 + react: 19.1.0 - '@reactflow/background@11.3.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/background@11.3.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/controls@11.2.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/core@11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -10821,55 +11139,55 @@ snapshots: d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/minimap@11.7.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/d3-selection': 3.0.11 '@types/d3-zoom': 3.0.8 classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/node-resizer@2.2.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@reactflow/node-toolbar@1.3.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) classcat: 5.0.5 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer - '@remixicon/react@4.6.0(react@19.0.0)': + '@remixicon/react@4.6.0(react@19.1.0)': dependencies: - react: 19.0.0 + react: 19.1.0 '@rgrove/parse-xml@4.2.0': {} @@ -10965,12 +11283,12 @@ snapshots: '@sentry/core@8.55.0': {} - '@sentry/react@8.55.0(react@19.0.0)': + '@sentry/react@8.55.0(react@19.1.0)': dependencies: '@sentry/browser': 8.55.0 '@sentry/core': 8.55.0 hoist-non-react-statics: 3.3.2 - react: 19.0.0 + react: 19.1.0 '@sentry/utils@8.55.0': dependencies: @@ -11011,9 +11329,9 @@ snapshots: storybook: 8.5.0 ts-dedent: 2.2.0 - '@storybook/addon-docs@8.5.0(@types/react@18.2.79)(storybook@8.5.0)': + '@storybook/addon-docs@8.5.0(@types/react@19.1.8)(storybook@8.5.0)': dependencies: - '@mdx-js/react': 3.1.0(@types/react@18.2.79)(react@18.3.1) + '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@18.3.1) '@storybook/blocks': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0) '@storybook/csf-plugin': 8.5.0(storybook@8.5.0) '@storybook/react-dom-shim': 8.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.0) @@ -11024,12 +11342,12 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.5.0(@types/react@18.2.79)(storybook@8.5.0)': + '@storybook/addon-essentials@8.5.0(@types/react@19.1.8)(storybook@8.5.0)': dependencies: '@storybook/addon-actions': 8.5.0(storybook@8.5.0) '@storybook/addon-backgrounds': 8.5.0(storybook@8.5.0) '@storybook/addon-controls': 8.5.0(storybook@8.5.0) - '@storybook/addon-docs': 8.5.0(@types/react@18.2.79)(storybook@8.5.0) + '@storybook/addon-docs': 8.5.0(@types/react@19.1.8)(storybook@8.5.0) '@storybook/addon-highlight': 8.5.0(storybook@8.5.0) '@storybook/addon-measure': 8.5.0(storybook@8.5.0) '@storybook/addon-outline': 8.5.0(storybook@8.5.0) @@ -11054,14 +11372,14 @@ snapshots: storybook: 8.5.0 ts-dedent: 2.2.0 - '@storybook/addon-links@8.5.0(react@19.0.0)(storybook@8.5.0)': + '@storybook/addon-links@8.5.0(react@19.1.0)(storybook@8.5.0)': dependencies: '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 storybook: 8.5.0 ts-dedent: 2.2.0 optionalDependencies: - react: 19.0.0 + react: 19.1.0 '@storybook/addon-measure@8.5.0(storybook@8.5.0)': dependencies: @@ -11103,17 +11421,17 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/blocks@8.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)': + '@storybook/blocks@8.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)': dependencies: '@storybook/csf': 0.1.12 - '@storybook/icons': 1.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@storybook/icons': 1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) storybook: 8.5.0 ts-dedent: 2.2.0 optionalDependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - '@storybook/builder-webpack5@8.5.0(esbuild@0.25.0)(storybook@8.5.0)(typescript@4.9.5)(uglify-js@3.19.3)': + '@storybook/builder-webpack5@8.5.0(esbuild@0.25.0)(storybook@8.5.0)(typescript@5.8.3)(uglify-js@3.19.3)': dependencies: '@storybook/core-webpack': 8.5.0(storybook@8.5.0) '@types/semver': 7.7.0 @@ -11123,7 +11441,7 @@ snapshots: constants-browserify: 1.0.0 css-loader: 6.11.0(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) es-module-lexer: 1.6.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) html-webpack-plugin: 5.6.3(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) magic-string: 0.30.17 path-browserify: 1.0.1 @@ -11141,7 +11459,7 @@ snapshots: webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -11196,10 +11514,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/icons@1.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@storybook/icons@1.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) '@storybook/instrumenter@8.5.0(storybook@8.5.0)': dependencies: @@ -11211,7 +11529,7 @@ snapshots: dependencies: storybook: 8.5.0 - '@storybook/nextjs@8.5.0(esbuild@0.25.0)(next@15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3)(storybook@8.5.0)(type-fest@4.39.1)(typescript@4.9.5)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3))': + '@storybook/nextjs@8.5.0(esbuild@0.25.0)(next@15.3.5(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3)(storybook@8.5.0)(type-fest@4.39.1)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3))': dependencies: '@babel/core': 7.26.10 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.10) @@ -11227,9 +11545,9 @@ snapshots: '@babel/preset-typescript': 7.27.0(@babel/core@7.26.10) '@babel/runtime': 7.27.0 '@pmmmwh/react-refresh-webpack-plugin': 0.5.16(react-refresh@0.14.2)(type-fest@4.39.1)(webpack-hot-middleware@2.26.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) - '@storybook/builder-webpack5': 8.5.0(esbuild@0.25.0)(storybook@8.5.0)(typescript@4.9.5)(uglify-js@3.19.3) - '@storybook/preset-react-webpack': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(esbuild@0.25.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5)(uglify-js@3.19.3) - '@storybook/react': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5) + '@storybook/builder-webpack5': 8.5.0(esbuild@0.25.0)(storybook@8.5.0)(typescript@5.8.3)(uglify-js@3.19.3) + '@storybook/preset-react-webpack': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(esbuild@0.25.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3)(uglify-js@3.19.3) + '@storybook/react': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3) '@storybook/test': 8.5.0(storybook@8.5.0) '@types/semver': 7.7.0 babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) @@ -11237,26 +11555,26 @@ snapshots: find-up: 5.0.0 image-size: 1.2.1 loader-utils: 3.3.1 - next: 15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3) + next: 15.3.5(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3) node-polyfill-webpack-plugin: 2.0.1(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) - pnp-webpack-plugin: 1.7.0(typescript@4.9.5) + pnp-webpack-plugin: 1.7.0(typescript@5.8.3) postcss: 8.5.3 - postcss-loader: 8.1.1(postcss@8.5.3)(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + postcss-loader: 8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) react-refresh: 0.14.2 resolve-url-loader: 5.0.0 sass-loader: 14.2.1(sass@1.86.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) semver: 7.7.1 storybook: 8.5.0 style-loader: 3.3.4(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) - styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.0.0) + styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0) ts-dedent: 2.2.0 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 optionalDependencies: sharp: 0.33.5 - typescript: 4.9.5 + typescript: 5.8.3 webpack: 5.99.5(esbuild@0.25.0)(uglify-js@3.19.3) transitivePeerDependencies: - '@rspack/core' @@ -11276,24 +11594,24 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(esbuild@0.25.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5)(uglify-js@3.19.3)': + '@storybook/preset-react-webpack@8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(esbuild@0.25.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3)(uglify-js@3.19.3)': dependencies: '@storybook/core-webpack': 8.5.0(storybook@8.5.0) - '@storybook/react': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) + '@storybook/react': 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)) '@types/semver': 7.7.0 find-up: 5.0.0 magic-string: 0.30.17 - react: 19.0.0 + react: 19.1.0 react-docgen: 7.1.1 - react-dom: 19.0.0(react@19.0.0) + react-dom: 19.1.0(react@19.1.0) resolve: 1.22.10 semver: 7.7.1 storybook: 8.5.0 tsconfig-paths: 4.2.0 webpack: 5.99.5(esbuild@0.25.0)(uglify-js@3.19.3) optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - '@storybook/test' - '@swc/core' @@ -11306,16 +11624,16 @@ snapshots: dependencies: storybook: 8.5.0 - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3))': dependencies: debug: 4.4.0 endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 micromatch: 4.0.8 - react-docgen-typescript: 2.2.2(typescript@4.9.5) + react-docgen-typescript: 2.2.2(typescript@5.8.3) tslib: 2.8.1 - typescript: 4.9.5 + typescript: 5.8.3 webpack: 5.99.5(esbuild@0.25.0)(uglify-js@3.19.3) transitivePeerDependencies: - supports-color @@ -11326,26 +11644,26 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.5.0 - '@storybook/react-dom-shim@8.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)': + '@storybook/react-dom-shim@8.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)': dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) storybook: 8.5.0 - '@storybook/react@8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0)(typescript@4.9.5)': + '@storybook/react@8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0)(typescript@5.8.3)': dependencies: '@storybook/components': 8.5.0(storybook@8.5.0) '@storybook/global': 5.0.0 '@storybook/manager-api': 8.5.0(storybook@8.5.0) '@storybook/preview-api': 8.5.0(storybook@8.5.0) - '@storybook/react-dom-shim': 8.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.5.0) + '@storybook/react-dom-shim': 8.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.5.0) '@storybook/theming': 8.5.0(storybook@8.5.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) storybook: 8.5.0 optionalDependencies: '@storybook/test': 8.5.0(storybook@8.5.0) - typescript: 4.9.5 + typescript: 5.8.3 '@storybook/test@8.5.0(storybook@8.5.0)': dependencies: @@ -11363,9 +11681,9 @@ snapshots: dependencies: storybook: 8.5.0 - '@stylistic/eslint-plugin@4.2.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@stylistic/eslint-plugin@4.2.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -11391,13 +11709,13 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)))': + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) '@tanstack/form-core@1.3.2': dependencies: @@ -11407,39 +11725,39 @@ snapshots: '@tanstack/query-devtools@5.72.2': {} - '@tanstack/react-form@1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-form@1.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/form-core': 1.3.2 - '@tanstack/react-store': 0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-store': 0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) decode-formdata: 0.9.0 devalue: 5.1.1 - react: 19.0.0 + react: 19.1.0 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.72.2(@tanstack/react-query@5.72.2(react@19.0.0))(react@19.0.0)': + '@tanstack/react-query-devtools@5.72.2(@tanstack/react-query@5.72.2(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/query-devtools': 5.72.2 - '@tanstack/react-query': 5.72.2(react@19.0.0) - react: 19.0.0 + '@tanstack/react-query': 5.72.2(react@19.1.0) + react: 19.1.0 - '@tanstack/react-query@5.72.2(react@19.0.0)': + '@tanstack/react-query@5.72.2(react@19.1.0)': dependencies: '@tanstack/query-core': 5.72.2 - react: 19.0.0 + react: 19.1.0 - '@tanstack/react-store@0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-store@0.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/store': 0.7.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - use-sync-external-store: 1.5.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) - '@tanstack/react-virtual@3.13.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-virtual@3.13.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/virtual-core': 3.13.6 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) '@tanstack/store@0.7.0': {} @@ -11476,15 +11794,15 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.0 '@testing-library/dom': 10.4.0 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) optionalDependencies: - '@types/react': 18.2.79 - '@types/react-dom': 18.2.25 + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: @@ -11747,34 +12065,31 @@ snapshots: '@types/parse-json@4.0.2': {} - '@types/prop-types@15.7.14': {} - '@types/qs@6.9.18': {} - '@types/react-dom@18.2.25': + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 '@types/react-slider@1.3.6': dependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 '@types/react-syntax-highlighter@15.5.13': dependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 '@types/react-window-infinite-loader@1.0.9': dependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 '@types/react-window': 1.8.8 '@types/react-window@1.8.8': dependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 - '@types/react@18.2.79': + '@types/react@19.1.8': dependencies: - '@types/prop-types': 15.7.14 csstype: 3.1.3 '@types/recordrtc@5.6.14': {} @@ -11808,32 +12123,70 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.29.1 eslint: 9.24.0(jiti@1.21.7) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/type-utils': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.36.0 + eslint: 9.24.0(jiti@1.21.7) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.29.1 debug: 4.4.0 eslint: 9.24.0(jiti@1.21.7) - typescript: 4.9.5 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.36.0 + debug: 4.4.0 + eslint: 9.24.0(jiti@1.21.7) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.36.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) + '@typescript-eslint/types': 8.36.0 + debug: 4.4.0 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11842,20 +12195,42 @@ snapshots: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/visitor-keys': 8.29.1 - '@typescript-eslint/type-utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@typescript-eslint/scope-manager@8.36.0': + dependencies: + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 + + '@typescript-eslint/tsconfig-utils@8.36.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + debug: 4.4.0 + eslint: 9.24.0(jiti@1.21.7) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.29.1(typescript@4.9.5) - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) debug: 4.4.0 eslint: 9.24.0(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.29.1': {} - '@typescript-eslint/typescript-estree@8.29.1(typescript@4.9.5)': + '@typescript-eslint/types@8.36.0': {} + + '@typescript-eslint/typescript-estree@8.29.1(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.29.1 '@typescript-eslint/visitor-keys': 8.29.1 @@ -11864,19 +12239,46 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)': + '@typescript-eslint/typescript-estree@8.36.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.36.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@1.21.7)) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/typescript-estree': 8.29.1(typescript@4.9.5) + '@typescript-eslint/typescript-estree': 8.29.1(typescript@5.8.3) + eslint: 9.24.0(jiti@1.21.7) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.24.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -11885,6 +12287,11 @@ snapshots: '@typescript-eslint/types': 8.29.1 eslint-visitor-keys: 4.2.0 + '@typescript-eslint/visitor-keys@8.36.0': + dependencies: + '@typescript-eslint/types': 8.36.0 + eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-darwin-arm64@1.4.1': @@ -11934,13 +12341,13 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.4.1': optional: true - '@vitest/eslint-plugin@1.1.42(@typescript-eslint/utils@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': + '@vitest/eslint-plugin@1.1.42(@typescript-eslint/utils@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': dependencies: - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) vitest: 3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 '@vitest/expect@2.0.5': dependencies: @@ -11956,13 +12363,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.1(vite@6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': + '@vitest/mocker@3.1.1(vite@6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1))': dependencies: '@vitest/spy': 3.1.1 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) '@vitest/pretty-format@2.0.5': dependencies: @@ -12163,14 +12570,14 @@ snapshots: - supports-color optional: true - ahooks@3.8.4(react@19.0.0): + ahooks@3.8.4(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 dayjs: 1.11.13 intersection-observer: 0.12.2 js-cookie: 3.0.5 lodash: 4.17.21 - react: 19.0.0 + react: 19.1.0 react-fast-compare: 3.2.2 resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 @@ -12488,12 +12895,7 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -12846,8 +13248,6 @@ snapshots: compare-versions@6.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} confbox@0.2.2: {} @@ -12891,20 +13291,27 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@9.0.0(typescript@4.9.5): + cosmiconfig@9.0.0(typescript@5.8.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 create-ecdh@4.0.4: dependencies: bn.js: 4.12.1 elliptic: 6.6.1 + create-hash@1.1.3: + dependencies: + cipher-base: 1.0.6 + inherits: 2.0.4 + ripemd160: 2.0.2 + sha.js: 2.4.11 + create-hash@1.2.0: dependencies: cipher-base: 1.0.6 @@ -12922,13 +13329,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - create-jest@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + create-jest@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -12959,7 +13366,7 @@ snapshots: diffie-hellman: 5.0.3 hash-base: 3.0.5 inherits: 2.0.4 - pbkdf2: 3.1.2 + pbkdf2: 3.1.3 public-encrypt: 4.0.3 randombytes: 2.1.0 randomfill: 1.0.4 @@ -13271,6 +13678,9 @@ snapshots: detect-libc@2.0.3: {} + detect-libc@2.0.4: + optional: true + detect-newline@3.1.0: {} devalue@5.1.1: {} @@ -13346,11 +13756,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - echarts-for-react@3.0.2(echarts@5.6.0)(react@19.0.0): + echarts-for-react@3.0.2(echarts@5.6.0)(react@19.1.0): dependencies: echarts: 5.6.0 fast-deep-equal: 3.1.3 - react: 19.0.0 + react: 19.1.0 size-sensor: 1.0.2 echarts@5.6.0: @@ -13621,21 +14031,21 @@ snapshots: '@eslint/compat': 1.2.8(eslint@9.24.0(jiti@1.21.7)) eslint: 9.24.0(jiti@1.21.7) - eslint-config-next@15.3.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-config-next@15.3.5(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@next/eslint-plugin-next': 15.3.0 + '@next/eslint-plugin-next': 15.3.5 '@rushstack/eslint-patch': 1.11.0 - '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.24.0(jiti@1.21.7)) eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@1.21.7)) optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -13653,7 +14063,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -13664,8 +14074,8 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) - eslint-plugin-import-x: 4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) + eslint-plugin-import-x: 4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) transitivePeerDependencies: - supports-color @@ -13679,14 +14089,14 @@ snapshots: dependencies: eslint: 9.24.0(jiti@1.21.7) - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -13706,11 +14116,11 @@ snapshots: eslint: 9.24.0(jiti@1.21.7) eslint-compat-utils: 0.5.1(eslint@9.24.0(jiti@1.21.7)) - eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-import-x@4.10.2(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: '@pkgr/core': 0.2.2 '@types/doctrine': 0.0.9 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) debug: 4.4.0 doctrine: 3.0.0 eslint: 9.24.0(jiti@1.21.7) @@ -13726,7 +14136,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -13737,7 +14147,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -13749,7 +14159,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -13819,10 +14229,10 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-perfectionist@4.11.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-perfectionist@4.11.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) natural-orderby: 5.0.0 transitivePeerDependencies: @@ -13839,66 +14249,66 @@ snapshots: tinyglobby: 0.2.12 yaml-eslint-parser: 1.3.0 - eslint-plugin-react-debug@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-react-debug@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-dom@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-react-dom@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) compare-versions: 6.1.1 eslint: 9.24.0(jiti@1.21.7) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks-extra@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-react-hooks-extra@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -13906,24 +14316,24 @@ snapshots: dependencies: eslint: 9.24.0(jiti@1.21.7) - eslint-plugin-react-naming-convention@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-react-naming-convention@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -13931,47 +14341,47 @@ snapshots: dependencies: eslint: 9.24.0(jiti@1.21.7) - eslint-plugin-react-web-api@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-react-web-api@1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@4.9.5))(typescript@4.9.5): + eslint-plugin-react-x@1.45.0(eslint@9.24.0(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.8.3))(typescript@5.8.3): dependencies: - '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/ast': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/core': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@eslint-react/eff': 1.45.0 - '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@eslint-react/jsx': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/kit': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/shared': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@eslint-react/var': 1.45.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/scope-manager': 8.29.1 - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) '@typescript-eslint/types': 8.29.1 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) compare-versions: 6.1.1 eslint: 9.24.0(jiti@1.21.7) - is-immutable-type: 5.0.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + is-immutable-type: 5.0.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) string-ts: 2.2.1 ts-pattern: 5.7.0 optionalDependencies: - ts-api-utils: 2.1.0(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14021,21 +14431,21 @@ snapshots: semver: 7.7.1 typescript: 5.8.3 - eslint-plugin-storybook@0.11.6(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + eslint-plugin-storybook@0.11.6(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: '@storybook/csf': 0.1.13 - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-tailwindcss@3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))): + eslint-plugin-tailwindcss@3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3))): dependencies: fast-glob: 3.3.3 postcss: 8.5.3 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) eslint-plugin-toml@0.12.0(eslint@9.24.0(jiti@1.21.7)): dependencies: @@ -14068,11 +14478,11 @@ snapshots: semver: 7.7.1 strip-indent: 4.0.0 - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7)): + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7)): dependencies: eslint: 9.24.0(jiti@1.21.7) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint-plugin-vue@10.0.0(eslint@9.24.0(jiti@1.21.7))(vue-eslint-parser@10.1.3(eslint@9.24.0(jiti@1.21.7))): dependencies: @@ -14115,6 +14525,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.24.0(jiti@1.21.7): dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0(jiti@1.21.7)) @@ -14377,7 +14789,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@8.0.0(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)): dependencies: '@babel/code-frame': 7.26.2 chalk: 4.1.2 @@ -14391,7 +14803,7 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.1 tapable: 2.2.1 - typescript: 4.9.5 + typescript: 5.8.3 webpack: 5.99.5(esbuild@0.25.0)(uglify-js@3.19.3) format@0.2.2: {} @@ -14577,6 +14989,10 @@ snapshots: has-unicode@2.0.1: optional: true + hash-base@2.0.2: + dependencies: + inherits: 2.0.4 + hash-base@3.0.5: dependencies: inherits: 2.0.4 @@ -14832,6 +15248,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + image-size@1.2.1: dependencies: queue: 6.0.2 @@ -14935,7 +15353,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.2 is-callable@1.2.7: {} @@ -14991,13 +15409,13 @@ snapshots: is-hexadecimal@2.0.1: {} - is-immutable-type@5.0.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + is-immutable-type@5.0.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/type-utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) - ts-api-utils: 2.1.0(typescript@4.9.5) - ts-declaration-location: 1.0.7(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 2.1.0(typescript@5.8.3) + ts-declaration-location: 1.0.7(typescript@5.8.3) + typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -15160,16 +15578,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + jest-cli@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + create-jest: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + jest-config: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -15179,7 +15597,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + jest-config@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: '@babel/core': 7.26.10 '@jest/test-sequencer': 29.7.0 @@ -15205,7 +15623,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 18.15.0 - ts-node: 10.9.2(@types/node@18.15.0)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@18.15.0)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -15431,12 +15849,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + jest@29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + jest-cli: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -16239,15 +16657,15 @@ snapshots: minimatch@10.0.1: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -16302,33 +16720,33 @@ snapshots: neo-async@2.6.2: {} - next-themes@0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - next@15.2.3(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.3): + next@15.3.5(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.3): dependencies: - '@next/env': 15.2.3 + '@next/env': 15.3.5 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 caniuse-lite: 1.0.30001713 postcss: 8.4.31 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.26.10)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.2.3 - '@next/swc-darwin-x64': 15.2.3 - '@next/swc-linux-arm64-gnu': 15.2.3 - '@next/swc-linux-arm64-musl': 15.2.3 - '@next/swc-linux-x64-gnu': 15.2.3 - '@next/swc-linux-x64-musl': 15.2.3 - '@next/swc-win32-arm64-msvc': 15.2.3 - '@next/swc-win32-x64-msvc': 15.2.3 + '@next/swc-darwin-arm64': 15.3.5 + '@next/swc-darwin-x64': 15.3.5 + '@next/swc-linux-arm64-gnu': 15.3.5 + '@next/swc-linux-arm64-musl': 15.3.5 + '@next/swc-linux-x64-gnu': 15.3.5 + '@next/swc-linux-x64-musl': 15.3.5 + '@next/swc-win32-arm64-msvc': 15.3.5 + '@next/swc-win32-x64-msvc': 15.3.5 sass: 1.86.3 - sharp: 0.33.5 + sharp: 0.34.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -16563,7 +16981,7 @@ snapshots: browserify-aes: 1.2.0 evp_bytestokey: 1.0.3 hash-base: 3.0.5 - pbkdf2: 3.1.2 + pbkdf2: 3.1.3 safe-buffer: 5.2.1 parse-entities@2.0.0: @@ -16644,13 +17062,14 @@ snapshots: pathval@2.0.0: {} - pbkdf2@3.1.2: + pbkdf2@3.1.3: dependencies: - create-hash: 1.2.0 + create-hash: 1.1.3 create-hmac: 1.1.7 - ripemd160: 2.0.2 + ripemd160: 2.0.1 safe-buffer: 5.2.1 sha.js: 2.4.11 + to-buffer: 1.2.1 pdfjs-dist@4.4.168: optionalDependencies: @@ -16696,9 +17115,9 @@ snapshots: pluralize@8.0.0: {} - pnp-webpack-plugin@1.7.0(typescript@4.9.5): + pnp-webpack-plugin@1.7.0(typescript@5.8.3): dependencies: - ts-pnp: 1.2.0(typescript@4.9.5) + ts-pnp: 1.2.0(typescript@5.8.3) transitivePeerDependencies: - typescript @@ -16738,17 +17157,17 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: lilconfig: 3.1.3 yaml: 2.7.1 optionalDependencies: postcss: 8.5.3 - ts-node: 10.9.2(@types/node@18.15.0)(typescript@4.9.5) + ts-node: 10.9.2(@types/node@18.15.0)(typescript@5.8.3) - postcss-loader@8.1.1(postcss@8.5.3)(typescript@4.9.5)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)): + postcss-loader@8.1.1(postcss@8.5.3)(typescript@5.8.3)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)): dependencies: - cosmiconfig: 9.0.0(typescript@4.9.5) + cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 postcss: 8.5.3 semver: 7.7.1 @@ -16831,8 +17250,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prismjs@1.27.0: {} - prismjs@1.30.0: {} process-nextick-args@2.0.1: {} @@ -16878,9 +17295,9 @@ snapshots: pure-rand@6.1.0: {} - qrcode.react@4.2.0(react@19.0.0): + qrcode.react@4.2.0(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 qs@6.14.0: dependencies: @@ -16909,24 +17326,24 @@ snapshots: range-parser@1.2.1: {} - re-resizable@6.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + re-resizable@6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - react-18-input-autosize@3.0.0(react@19.0.0): + react-18-input-autosize@3.0.0(react@19.1.0): dependencies: prop-types: 15.8.1 - react: 19.0.0 + react: 19.1.0 - react-confetti@6.4.0(react@19.0.0): + react-confetti@6.4.0(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 tween-functions: 1.2.0 - react-docgen-typescript@2.2.2(typescript@4.9.5): + react-docgen-typescript@2.2.2(typescript@5.8.3): dependencies: - typescript: 4.9.5 + typescript: 5.8.3 react-docgen@7.1.1: dependencies: @@ -16949,63 +17366,63 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@19.0.0(react@19.0.0): + react-dom@19.1.0(react@19.1.0): dependencies: - react: 19.0.0 - scheduler: 0.25.0 + react: 19.1.0 + scheduler: 0.26.0 - react-draggable@4.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-draggable@4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: clsx: 1.2.1 prop-types: 15.8.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - react-easy-crop@5.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-easy-crop@5.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: normalize-wheel: 1.0.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) tslib: 2.8.1 - react-error-boundary@3.1.4(react@19.0.0): + react-error-boundary@3.1.4(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 - react: 19.0.0 + react: 19.1.0 - react-error-boundary@4.1.2(react@19.0.0): + react-error-boundary@4.1.2(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 - react: 19.0.0 + react: 19.1.0 react-fast-compare@3.2.2: {} - react-headless-pagination@1.1.6(react@19.0.0): + react-headless-pagination@1.1.6(react@19.1.0): dependencies: clsx: 2.1.1 - react: 19.0.0 + react: 19.1.0 - react-hook-form@7.55.0(react@19.0.0): + react-hook-form@7.55.0(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 - react-hotkeys-hook@4.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-hotkeys-hook@4.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - react-i18next@15.4.1(i18next@23.16.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-i18next@15.4.1(i18next@23.16.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 html-parse-stringify: 3.0.1 i18next: 23.16.8 - react: 19.0.0 + react: 19.1.0 optionalDependencies: - react-dom: 19.0.0(react@19.0.0) + react-dom: 19.1.0(react@19.1.0) - react-infinite-scroll-component@6.1.0(react@19.0.0): + react-infinite-scroll-component@6.1.0(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 throttle-debounce: 2.3.0 react-is@16.13.1: {} @@ -17014,16 +17431,16 @@ snapshots: react-is@18.3.1: {} - react-markdown@9.1.0(@types/react@18.2.79)(react@19.0.0): + react-markdown@9.1.0(@types/react@19.1.8)(react@19.1.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.2.79 + '@types/react': 19.1.8 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 - react: 19.0.0 + react: 19.1.0 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -17032,22 +17449,22 @@ snapshots: transitivePeerDependencies: - supports-color - react-multi-email@1.0.25(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-multi-email@1.0.25(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) react-papaparse@4.4.0: dependencies: '@types/papaparse': 5.3.15 papaparse: 5.5.2 - react-pdf-highlighter@8.0.0-rc.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-pdf-highlighter@8.0.0-rc.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: pdfjs-dist: 4.4.168 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - react-rnd: 10.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-rnd: 10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) ts-debounce: 4.0.0 transitivePeerDependencies: - encoding @@ -17055,82 +17472,82 @@ snapshots: react-refresh@0.14.2: {} - react-rnd@10.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-rnd@10.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - re-resizable: 6.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - react-draggable: 4.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + re-resizable: 6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-draggable: 4.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tslib: 2.6.2 - react-slider@2.0.6(react@19.0.0): + react-slider@2.0.6(react@19.1.0): dependencies: prop-types: 15.8.1 - react: 19.0.0 + react: 19.1.0 - react-sortablejs@6.1.4(@types/sortablejs@1.15.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sortablejs@1.15.6): + react-sortablejs@6.1.4(@types/sortablejs@1.15.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sortablejs@1.15.6): dependencies: '@types/sortablejs': 1.15.8 classnames: 2.3.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) sortablejs: 1.15.6 tiny-invariant: 1.2.0 - react-syntax-highlighter@15.6.1(react@19.0.0): + react-syntax-highlighter@15.6.1(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 highlight.js: 10.7.3 highlightjs-vue: 1.0.0 lowlight: 1.20.0 prismjs: 1.30.0 - react: 19.0.0 + react: 19.1.0 refractor: 3.6.0 - react-textarea-autosize@8.5.9(@types/react@18.2.79)(react@19.0.0): + react-textarea-autosize@8.5.9(@types/react@19.1.8)(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 - react: 19.0.0 - use-composed-ref: 1.4.0(@types/react@18.2.79)(react@19.0.0) - use-latest: 1.3.0(@types/react@18.2.79)(react@19.0.0) + react: 19.1.0 + use-composed-ref: 1.4.0(@types/react@19.1.8)(react@19.1.0) + use-latest: 1.3.0(@types/react@19.1.8)(react@19.1.0) transitivePeerDependencies: - '@types/react' - react-tooltip@5.8.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-tooltip@5.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@floating-ui/dom': 1.1.1 classnames: 2.5.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - react-window-infinite-loader@1.0.10(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-window-infinite-loader@1.0.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) - react-window@1.8.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + react-window@1.8.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 memoize-one: 5.2.1 - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) react@18.3.1: dependencies: loose-envify: 1.4.0 - react@19.0.0: {} + react@19.1.0: {} - reactflow@11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + reactflow@11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@reactflow/background': 11.3.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/controls': 11.2.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/core': 11.11.4(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/minimap': 11.7.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/node-resizer': 2.2.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.2.79)(immer@9.0.21)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + '@reactflow/background': 11.3.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@reactflow/controls': 11.2.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@reactflow/core': 11.11.4(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@reactflow/minimap': 11.7.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@reactflow/node-resizer': 2.2.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@reactflow/node-toolbar': 1.3.14(@types/react@19.1.8)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) transitivePeerDependencies: - '@types/react' - immer @@ -17247,7 +17664,7 @@ snapshots: dependencies: hastscript: 6.0.0 parse-entities: 2.0.0 - prismjs: 1.27.0 + prismjs: 1.30.0 regenerate-unicode-properties@10.2.0: dependencies: @@ -17441,6 +17858,11 @@ snapshots: dependencies: glob: 7.2.3 + ripemd160@2.0.1: + dependencies: + hash-base: 2.0.2 + inherits: 2.0.4 + ripemd160@2.0.2: dependencies: hash-base: 3.0.5 @@ -17531,7 +17953,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - scheduler@0.25.0: {} + scheduler@0.26.0: {} schema-utils@3.3.0: dependencies: @@ -17558,6 +17980,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -17622,6 +18046,36 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + optional: true + shave@5.0.4: {} shebang-command@2.0.0: @@ -17891,10 +18345,10 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.0.0): + styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.1.0): dependencies: client-only: 0.0.1 - react: 19.0.0 + react: 19.1.0 optionalDependencies: '@babel/core': 7.26.10 @@ -17920,11 +18374,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.3(react@19.0.0): + swr@2.3.3(react@19.1.0): dependencies: dequal: 2.0.3 - react: 19.0.0 - use-sync-external-store: 1.5.0(react@19.0.0) + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) synckit@0.10.3: dependencies: @@ -17940,7 +18394,7 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)): + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -17959,7 +18413,7 @@ snapshots: postcss: 8.5.3 postcss-import: 15.1.0(postcss@8.5.3) postcss-js: 4.0.1(postcss@8.5.3) - postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3)) postcss-nested: 6.2.0(postcss@8.5.3) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -18039,8 +18493,20 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@7.0.9: {} + + tldts@7.0.9: + dependencies: + tldts-core: 7.0.9 + tmpl@1.0.5: {} + to-buffer@1.2.1: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -18058,22 +18524,22 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.1.0(typescript@4.9.5): + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: - typescript: 4.9.5 + typescript: 5.8.3 ts-debounce@4.0.0: {} - ts-declaration-location@1.0.7(typescript@4.9.5): + ts-declaration-location@1.0.7(typescript@5.8.3): dependencies: picomatch: 4.0.2 - typescript: 4.9.5 + typescript: 5.8.3 ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5): + ts-node@10.9.2(@types/node@18.15.0)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -18087,15 +18553,15 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.9.5 + typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 ts-pattern@5.7.0: {} - ts-pnp@1.2.0(typescript@4.9.5): + ts-pnp@1.2.0(typescript@5.8.3): optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -18172,18 +18638,16 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5): + typescript-eslint@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.29.1(@typescript-eslint/parser@8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5))(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/parser': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) - '@typescript-eslint/utils': 8.29.1(eslint@9.24.0(jiti@1.21.7))(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3) eslint: 9.24.0(jiti@1.21.7) - typescript: 4.9.5 + typescript: 5.8.3 transitivePeerDependencies: - supports-color - typescript@4.9.5: {} - typescript@5.8.3: {} ufo@1.6.1: {} @@ -18299,35 +18763,35 @@ snapshots: punycode: 1.4.1 qs: 6.14.0 - use-composed-ref@1.4.0(@types/react@18.2.79)(react@19.0.0): + use-composed-ref@1.4.0(@types/react@19.1.8)(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 optionalDependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 - use-context-selector@2.0.0(react@19.0.0)(scheduler@0.23.2): + use-context-selector@2.0.0(react@19.1.0)(scheduler@0.23.2): dependencies: - react: 19.0.0 + react: 19.1.0 scheduler: 0.23.2 - use-isomorphic-layout-effect@1.2.0(@types/react@18.2.79)(react@19.0.0): + use-isomorphic-layout-effect@1.2.0(@types/react@19.1.8)(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 optionalDependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 - use-latest@1.3.0(@types/react@18.2.79)(react@19.0.0): + use-latest@1.3.0(@types/react@19.1.8)(react@19.1.0): dependencies: - react: 19.0.0 - use-isomorphic-layout-effect: 1.2.0(@types/react@18.2.79)(react@19.0.0) + react: 19.1.0 + use-isomorphic-layout-effect: 1.2.0(@types/react@19.1.8)(react@19.1.0) optionalDependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 use-strict@1.0.1: {} - use-sync-external-store@1.5.0(react@19.0.0): + use-sync-external-store@1.5.0(react@19.1.0): dependencies: - react: 19.0.0 + react: 19.1.0 util-deprecate@1.0.2: {} @@ -18353,9 +18817,9 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 - valibot@1.0.0(typescript@4.9.5): + valibot@1.0.0(typescript@5.8.3): optionalDependencies: - typescript: 4.9.5 + typescript: 5.8.3 validate-npm-package-license@3.0.4: dependencies: @@ -18389,7 +18853,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) transitivePeerDependencies: - '@types/node' - jiti @@ -18404,7 +18868,7 @@ snapshots: - tsx - yaml - vite@6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1): + vite@6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1): dependencies: esbuild: 0.25.2 postcss: 8.5.3 @@ -18420,7 +18884,7 @@ snapshots: vitest@3.1.1(@types/debug@4.1.12)(@types/node@18.15.0)(happy-dom@17.4.4)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1): dependencies: '@vitest/expect': 3.1.1 - '@vitest/mocker': 3.1.1(vite@6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) + '@vitest/mocker': 3.1.1(vite@6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1)) '@vitest/pretty-format': 3.1.1 '@vitest/runner': 3.1.1 '@vitest/snapshot': 3.1.1 @@ -18436,7 +18900,7 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.6(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) + vite: 6.2.7(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) vite-node: 3.1.1(@types/node@18.15.0)(jiti@1.21.7)(sass@1.86.3)(terser@5.39.0)(yaml@2.7.1) why-is-node-running: 2.3.0 optionalDependencies: @@ -18703,16 +19167,16 @@ snapshots: dependencies: tslib: 2.3.0 - zundo@2.3.0(zustand@4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0)): + zundo@2.3.0(zustand@4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0)): dependencies: - zustand: 4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0) + zustand: 4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0) - zustand@4.5.6(@types/react@18.2.79)(immer@9.0.21)(react@19.0.0): + zustand@4.5.6(@types/react@19.1.8)(immer@9.0.21)(react@19.1.0): dependencies: - use-sync-external-store: 1.5.0(react@19.0.0) + use-sync-external-store: 1.5.0(react@19.1.0) optionalDependencies: - '@types/react': 18.2.79 + '@types/react': 19.1.8 immer: 9.0.21 - react: 19.0.0 + react: 19.1.0 zwitch@2.0.4: {} diff --git a/web/public/embed.js b/web/public/embed.js index 1efa541a88..e41405dbf8 100644 --- a/web/public/embed.js +++ b/web/public/embed.js @@ -112,9 +112,21 @@ return compressedSystemVariables; } + async function getCompressedUserVariablesFromConfig() { + const userVariables = config?.userVariables || {}; + const compressedUserVariables = {}; + await Promise.all( + Object.entries(userVariables).map(async ([key, value]) => { + compressedUserVariables[`user.${key}`] = await compressAndEncodeBase64(value); + }) + ); + return compressedUserVariables; + } + const params = new URLSearchParams({ ...await getCompressedInputsFromConfig(), - ...await getCompressedSystemVariablesFromConfig() + ...await getCompressedSystemVariablesFromConfig(), + ...await getCompressedUserVariablesFromConfig() }); const baseUrl = diff --git a/web/public/embed.min.js b/web/public/embed.min.js index b2781ee47d..b1d6f56920 100644 --- a/web/public/embed.min.js +++ b/web/public/embed.min.js @@ -18,7 +18,7 @@ transition-property: width, height; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; - `;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function d(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function r(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(` + `;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})(),...await(async()=>{var e=y?.userVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["user."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function r(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function d(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(` #${n.id} { position: fixed; bottom: var(--${n.id}-bottom, 1rem); @@ -33,10 +33,10 @@ cursor: pointer; z-index: 2147483647; } - `),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + `),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),r()):(n.appendChild(s()),r(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/> </svg> <svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> - `,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",e=>{e.preventDefault(),t()},{passive:!1}),y.draggable){var a=n;var l=y.dragAxis||"both";let s,d,t,r;function o(e){u=!1,r=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,d=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,d=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-r;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,d())}),document.getElementById(h)||r()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})(); \ No newline at end of file + `,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",e=>{e.preventDefault(),t()},{passive:!1}),y.draggable){var a=n;var l=y.dragAxis||"both";let s,r,t,d;function o(e){u=!1,d=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,r=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,r=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-d;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-r):(e=n.clientX-s,window.innerHeight-n.clientY-r);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: 40rem; /* Match mobile breakpoint*/\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,r())}),document.getElementById(h)||d()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})(); \ No newline at end of file diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 865909d2f9..36999bf8f3 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -86,5 +86,8 @@ export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled } enabled: !!appId && enabled, staleTime: 0, gcTime: 0, + initialData: { + result: !enabled, + }, }) } diff --git a/web/service/base.ts b/web/service/base.ts index c3cafe600b..49377be912 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -108,12 +108,13 @@ function unicodeToChar(text: string) { }) } -function requiredWebSSOLogin(message?: string) { - removeAccessToken() +function requiredWebSSOLogin(message?: string, code?: number) { const params = new URLSearchParams() - params.append('redirect_url', globalThis.location.pathname) + params.append('redirect_url', encodeURIComponent(`${globalThis.location.pathname}${globalThis.location.search}`)) if (message) params.append('message', message) + if (code) + params.append('code', String(code)) globalThis.location.href = `/webapp-signin?${params.toString()}` } @@ -403,10 +404,12 @@ export const ssePost = async ( res.json().then((data: any) => { if (isPublicAPI) { if (data.code === 'web_app_access_denied') - requiredWebSSOLogin(data.message) + requiredWebSSOLogin(data.message, 403) - if (data.code === 'web_sso_auth_required') + if (data.code === 'web_sso_auth_required') { + removeAccessToken() requiredWebSSOLogin() + } if (data.code === 'unauthorized') { removeAccessToken() @@ -484,10 +487,11 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther const { code, message } = errRespData // webapp sso if (code === 'web_app_access_denied') { - requiredWebSSOLogin(message) + requiredWebSSOLogin(message, 403) return Promise.reject(err) } if (code === 'web_sso_auth_required') { + removeAccessToken() requiredWebSSOLogin() return Promise.reject(err) } diff --git a/web/service/common.ts b/web/service/common.ts index 700cd4bf51..e071d556d1 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -337,8 +337,8 @@ export const verifyWebAppForgotPasswordToken: Fetcher<CommonResponse & { is_vali export const changeWebAppPasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => post<CommonResponse>(url, { body }, { isPublicAPI: true }) -export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { - return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) +export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: boolean) => { + return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic, silent }) } export const sendEMailLoginCode = (email: string, language = 'en-US') => diff --git a/web/service/knowledge/use-document.ts b/web/service/knowledge/use-document.ts index 6dabe7d872..e53a5ebde7 100644 --- a/web/service/knowledge/use-document.ts +++ b/web/service/knowledge/use-document.ts @@ -5,6 +5,7 @@ import { import { del, get, patch } from '../base' import { useInvalid } from '../use-base' import type { MetadataType, SortType } from '../datasets' +import { pauseDocIndexing, resumeDocIndexing } from '../datasets' import type { DocumentDetailResponse, DocumentListResponse, UpdateDocumentBatchParams } from '@/models/datasets' import { DocumentActionType } from '@/models/datasets' import type { CommonResponse } from '@/models/common' @@ -130,3 +131,23 @@ export const useDocumentMetadata = (payload: { export const useInvalidDocumentDetailKey = () => { return useInvalid(useDocumentDetailKey) } + +export const useDocumentPause = () => { + return useMutation({ + mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { + if (!datasetId || !documentId) + throw new Error('datasetId and documentId are required') + return pauseDocIndexing({ datasetId, documentId }) as Promise<CommonResponse> + }, + }) +} + +export const useDocumentResume = () => { + return useMutation({ + mutationFn: ({ datasetId, documentId }: UpdateDocumentBatchParams) => { + if (!datasetId || !documentId) + throw new Error('datasetId and documentId are required') + return resumeDocIndexing({ datasetId, documentId }) as Promise<CommonResponse> + }, + }) +} diff --git a/web/service/refresh-token.ts b/web/service/refresh-token.ts index 4f295f3f81..7eff08b52f 100644 --- a/web/service/refresh-token.ts +++ b/web/service/refresh-token.ts @@ -1,4 +1,4 @@ -import { apiPrefix } from '@/config' +import { API_PREFIX } from '@/config' import { fetchWithRetry } from '@/utils' const LOCAL_STORAGE_KEY = 'is_other_tab_refreshing' @@ -46,7 +46,7 @@ async function getNewAccessToken(timeout: number): Promise<void> { // it can lead to an infinite loop if the refresh attempt also returns 401. // To avoid this, handle token refresh separately in a dedicated function // that does not call baseFetch and uses a single retry mechanism. - const [error, ret] = await fetchWithRetry(globalThis.fetch(`${apiPrefix}/refresh-token`, { + const [error, ret] = await fetchWithRetry(globalThis.fetch(`${API_PREFIX}/refresh-token`, { method: 'POST', headers: { 'Content-Type': 'application/json;utf-8', diff --git a/web/service/tools.ts b/web/service/tools.ts index 38dcf382e6..6a88d8d567 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -124,6 +124,10 @@ export const fetchAllWorkflowTools = () => { return get<ToolWithProvider[]>('/workspaces/current/tools/workflow') } +export const fetchAllMCPTools = () => { + return get<ToolWithProvider[]>('/workspaces/current/tools/mcp') +} + export const fetchLabelList = () => { return get<Label[]>('/workspaces/current/tool-labels') } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index ecfdbcf993..ff092bb037 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react' import type { + FormOption, ModelProvider, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fetchModelProviderModelList } from '@/service/common' @@ -477,7 +478,7 @@ export const usePluginTaskList = (category?: PluginType) => { refreshPluginList(category ? { category } as any : undefined, !category) } } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRefetching]) const handleRefetch = useCallback(() => { @@ -571,3 +572,17 @@ export const usePluginInfo = (providerName?: string) => { enabled: !!providerName, }) } + +export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => { + return useMutation({ + mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', { + params: { + plugin_id, + provider, + action, + parameter, + provider_type, + }, + }), + }) +} diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index ceaa4b14b3..64a3ce7a1f 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -1,9 +1,11 @@ -import { get, post } from './base' +import { del, get, post, put } from './base' import type { Collection, + MCPServerDetail, Tool, } from '@/app/components/tools/types' import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { AppIconType } from '@/types/app' import { useInvalid } from './use-base' import { useMutation, @@ -61,6 +63,191 @@ export const useInvalidateAllWorkflowTools = () => { return useInvalid(useAllWorkflowToolsKey) } +const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools'] +export const useAllMCPTools = () => { + return useQuery<ToolWithProvider[]>({ + queryKey: useAllMCPToolsKey, + queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/mcp'), + }) +} + +export const useInvalidateAllMCPTools = () => { + return useInvalid(useAllMCPToolsKey) +} + +export const useCreateMCP = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + }) => { + return post<ToolWithProvider>('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + }) +} + +export const useUpdateMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + provider_id: string + }) => { + return put('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + onSuccess, + }) +} + +export const useDeleteMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete-mcp'], + mutationFn: (id: string) => { + return del('/workspaces/current/tool-provider/mcp', { + body: { + provider_id: id, + }, + }) + }, + onSuccess, + }) +} + +export const useAuthorizeMCP = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'authorize-mcp'], + mutationFn: (payload: { provider_id: string; }) => { + return post<{ result?: string; authorization_url?: string }>('/workspaces/current/tool-provider/mcp/auth', { + body: payload, + }) + }, + }) +} + +export const useUpdateMCPAuthorizationToken = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], + mutationFn: (payload: { provider_id: string; authorization_code: string }) => { + return get<MCPServerDetail>('/workspaces/current/tool-provider/mcp/token', { + params: { + ...payload, + }, + }) + }, + }) +} + +export const useMCPTools = (providerID: string) => { + return useQuery({ + enabled: !!providerID, + queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], + queryFn: () => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`), + }) +} +export const useInvalidateMCPTools = () => { + const queryClient = useQueryClient() + return (providerID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], + }) + } +} + +export const useUpdateMCPTools = () => { + return useMutation({ + mutationFn: (providerID: string) => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`), + }) +} + +export const useMCPServerDetail = (appID: string) => { + return useQuery<MCPServerDetail>({ + queryKey: [NAME_SPACE, 'MCPServerDetail', appID], + queryFn: () => get<MCPServerDetail>(`/apps/${appID}/server`), + }) +} + +export const useInvalidateMCPServerDetail = () => { + const queryClient = useQueryClient() + return (appID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'MCPServerDetail', appID], + }) + } +} + +export const useCreateMCPServer = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-mcp-server'], + mutationFn: (payload: { + appID: string + description: string + parameters?: Record<string, string> + }) => { + const { appID, ...rest } = payload + return post(`apps/${appID}/server`, { + body: { + ...rest, + }, + }) + }, + }) +} + +export const useUpdateMCPServer = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-mcp-server'], + mutationFn: (payload: { + appID: string + id: string + description?: string + status?: string + parameters?: Record<string, string> + }) => { + const { appID, ...rest } = payload + return put(`apps/${appID}/server`, { + body: { + ...rest, + }, + }) + }, + }) +} + +export const useRefreshMCPServerCode = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], + mutationFn: (appID: string) => { + return get<MCPServerDetail>(`apps/${appID}/server/refresh`) + }, + }) +} + export const useBuiltinProviderInfo = (providerName: string) => { return useQuery({ queryKey: [NAME_SPACE, 'builtin-provider-info', providerName], diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts index 4321552cc7..2c8f86ae5d 100644 --- a/web/service/use-workflow.ts +++ b/web/service/use-workflow.ts @@ -1,15 +1,17 @@ -import { del, get, patch, post } from './base' -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' +import { del, get, patch, post, put } from './base' +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { FetchWorkflowDraftPageParams, FetchWorkflowDraftPageResponse, FetchWorkflowDraftResponse, + NodeTracing, PublishWorkflowParams, UpdateWorkflowParams, + VarInInspect, WorkflowConfigResponse, } from '@/types/workflow' import type { CommonResponse } from '@/models/common' -import { useReset } from './use-base' +import { useInvalid, useReset } from './use-base' const NAME_SPACE = 'workflow' @@ -21,6 +23,16 @@ export const useAppWorkflow = (appID: string) => { }) } +export const useInvalidateAppWorkflow = () => { + const queryClient = useQueryClient() + return (appID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'publish', appID], + }) + } +} + export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => { return useQuery({ queryKey: [NAME_SPACE, 'config', appId], @@ -85,3 +97,120 @@ export const usePublishWorkflow = (appId: string) => { }), }) } + +const useLastRunKey = [NAME_SPACE, 'last-run'] +export const useLastRun = (appID: string, nodeId: string, enabled: boolean) => { + return useQuery<NodeTracing>({ + enabled, + queryKey: [...useLastRunKey, appID, nodeId], + queryFn: async () => { + return get(`apps/${appID}/workflows/draft/nodes/${nodeId}/last-run`, {}, { + silent: true, + }) + }, + retry: 0, + }) +} + +export const useInvalidLastRun = (appId: string, nodeId: string) => { + return useInvalid([NAME_SPACE, 'last-run', appId, nodeId]) +} + +// Rerun workflow or change the version of workflow +export const useInvalidAllLastRun = (appId: string) => { + return useInvalid([NAME_SPACE, 'last-run', appId]) +} + +const useConversationVarValuesKey = [NAME_SPACE, 'conversation-variable'] + +export const useConversationVarValues = (url?: string) => { + return useQuery({ + enabled: !!url, + queryKey: [...useConversationVarValuesKey, url], + queryFn: async () => { + const { items } = (await get(url || '')) as { items: VarInInspect[] } + return items + }, + }) +} + +export const useInvalidateConversationVarValues = (url: string) => { + return useInvalid([...useConversationVarValuesKey, url]) +} + +export const useResetConversationVar = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'reset conversation var', appId], + mutationFn: async (varId: string) => { + return put(`apps/${appId}/workflows/draft/variables/${varId}/reset`) + }, + }) +} + +export const useResetToLastRunValue = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'reset to last run value', appId], + mutationFn: async (varId: string): Promise<{ value: any }> => { + return put(`apps/${appId}/workflows/draft/variables/${varId}/reset`) + }, + }) +} + +export const useSysVarValuesKey = [NAME_SPACE, 'sys-variable'] +export const useSysVarValues = (url?: string) => { + return useQuery({ + enabled: !!url, + queryKey: [...useSysVarValuesKey, url], + queryFn: async () => { + const { items } = (await get(url || '')) as { items: VarInInspect[] } + return items + }, + }) +} + +export const useInvalidateSysVarValues = (url: string) => { + return useInvalid([...useSysVarValuesKey, url]) +} + +export const useDeleteAllInspectorVars = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete all inspector vars', appId], + mutationFn: async () => { + return del(`apps/${appId}/workflows/draft/variables`) + }, + }) +} + +export const useDeleteNodeInspectorVars = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete node inspector vars', appId], + mutationFn: async (nodeId: string) => { + return del(`apps/${appId}/workflows/draft/nodes/${nodeId}/variables`) + }, + }) +} + +export const useDeleteInspectVar = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete inspector var', appId], + mutationFn: async (varId: string) => { + return del(`apps/${appId}/workflows/draft/variables/${varId}`) + }, + }) +} + +// edit the name or value of the inspector var +export const useEditInspectorVar = (appId: string) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'edit inspector var', appId], + mutationFn: async ({ varId, ...rest }: { + varId: string + name?: string + value?: any + }) => { + return patch(`apps/${appId}/workflows/draft/variables/${varId}`, { + body: rest, + }) + }, + }) +} diff --git a/web/service/workflow.ts b/web/service/workflow.ts index 7d10394246..7d504b2f4d 100644 --- a/web/service/workflow.ts +++ b/web/service/workflow.ts @@ -9,6 +9,7 @@ import type { WorkflowRunHistoryResponse, } from '@/types/workflow' import type { BlockEnum } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' export const fetchWorkflowDraft = (url: string) => { return get(url, {}, { silent: true }) as Promise<FetchWorkflowDraftResponse> @@ -70,3 +71,31 @@ export const fetchCurrentValueOfConversationVariable: Fetcher<ConversationVariab }> = ({ url, params }) => { return get<ConversationVariableResponse>(url, { params }) } + +const fetchAllInspectVarsOnePage = async (appId: string, page: number): Promise<{ total: number, items: VarInInspect[] }> => { + return get(`apps/${appId}/workflows/draft/variables`, { + params: { page, limit: 100 }, + }) +} +export const fetchAllInspectVars = async (appId: string): Promise<VarInInspect[]> => { + const res = await fetchAllInspectVarsOnePage(appId, 1) + const { items, total } = res + if (total <= 100) + return items + + const pageCount = Math.ceil(total / 100) + const promises = [] + for (let i = 2; i <= pageCount; i++) + promises.push(fetchAllInspectVarsOnePage(appId, i)) + + const restData = await Promise.all(promises) + restData.forEach(({ items: item }) => { + items.push(...item) + }) + return items +} + +export const fetchNodeInspectVars = async (appId: string, nodeId: string): Promise<VarInInspect[]> => { + const { items } = (await get(`apps/${appId}/workflows/draft/nodes/${nodeId}/variables`)) as { items: VarInInspect[] } + return items +} diff --git a/web/tailwind-common-config.ts b/web/tailwind-common-config.ts index 3f64afcc29..350c1c29d2 100644 --- a/web/tailwind-common-config.ts +++ b/web/tailwind-common-config.ts @@ -71,6 +71,7 @@ const config = { boxShadow: { 'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)', 'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)', + 'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)', 'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)', 'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)', 'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)', @@ -89,6 +90,9 @@ const config = { fontSize: { '2xs': '0.625rem', }, + backgroundColor: { + 'background-gradient-bg-fill-chat-bubble-bg-3': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-3)', + }, backgroundImage: { 'chatbot-bg': 'var(--color-chatbot-bg)', 'chat-bubble-bg': 'var(--color-chat-bubble-bg)', diff --git a/web/themes/dark.css b/web/themes/dark.css index fbfacf1f45..d204838e5e 100644 --- a/web/themes/dark.css +++ b/web/themes/dark.css @@ -1,274 +1,278 @@ /* Attention: Generate by code. Don't update by hand!!! */ html[data-theme="dark"] { - --color-components-input-bg-normal: #ffffff14; - --color-components-input-text-placeholder: #c8ceda4d; - --color-components-input-bg-hover: #ffffff08; - --color-components-input-bg-active: #ffffff0d; + --color-components-input-bg-normal: rgb(255 255 255 / 0.08); + --color-components-input-text-placeholder: rgb(200 206 218 / 0.3); + --color-components-input-bg-hover: rgb(255 255 255 / 0.03); + --color-components-input-bg-active: rgb(255 255 255 / 0.05); --color-components-input-border-active: #747481; --color-components-input-border-destructive: #f97066; --color-components-input-text-filled: #f4f4f5; - --color-components-input-bg-destructive: #ffffff03; - --color-components-input-bg-disabled: #ffffff08; - --color-components-input-text-disabled: #c8ceda4d; - --color-components-input-text-filled-disabled: #c8ceda99; + --color-components-input-bg-destructive: rgb(255 255 255 / 0.01); + --color-components-input-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-input-text-disabled: rgb(200 206 218 / 0.3); + --color-components-input-text-filled-disabled: rgb(200 206 218 / 0.6); --color-components-input-border-hover: #3a3a40; --color-components-input-border-active-prompt-1: #36bffa; --color-components-input-border-active-prompt-2: #296dff; - --color-components-kbd-bg-gray: #ffffff08; - --color-components-kbd-bg-white: #ffffff1f; + --color-components-kbd-bg-gray: rgb(255 255 255 / 0.03); + --color-components-kbd-bg-white: rgb(255 255 255 / 0.12); - --color-components-tooltip-bg: #18181bf2; + --color-components-tooltip-bg: rgb(24 24 27 / 0.95); - --color-components-button-primary-text: #fffffff2; + --color-components-button-primary-text: rgb(255 255 255 / 0.95); --color-components-button-primary-bg: #155aef; - --color-components-button-primary-border: #ffffff1f; + --color-components-button-primary-border: rgb(255 255 255 / 0.12); --color-components-button-primary-bg-hover: #296dff; - --color-components-button-primary-border-hover: #ffffff33; - --color-components-button-primary-bg-disabled: #ffffff08; - --color-components-button-primary-border-disabled: #ffffff14; - --color-components-button-primary-text-disabled: #ffffff33; - - --color-components-button-secondary-text: #ffffffcc; - --color-components-button-secondary-text-disabled: #ffffff33; - --color-components-button-secondary-bg: #ffffff1f; - --color-components-button-secondary-bg-hover: #ffffff33; - --color-components-button-secondary-bg-disabled: #ffffff08; - --color-components-button-secondary-border: #ffffff14; - --color-components-button-secondary-border-hover: #ffffff1f; - --color-components-button-secondary-border-disabled: #ffffff0d; + --color-components-button-primary-border-hover: rgb(255 255 255 / 0.2); + --color-components-button-primary-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-button-primary-border-disabled: rgb(255 255 255 / 0.08); + --color-components-button-primary-text-disabled: rgb(255 255 255 / 0.2); + + --color-components-button-secondary-text: rgb(255 255 255 / 0.8); + --color-components-button-secondary-text-disabled: rgb(255 255 255 / 0.2); + --color-components-button-secondary-bg: rgb(255 255 255 / 0.12); + --color-components-button-secondary-bg-hover: rgb(255 255 255 / 0.2); + --color-components-button-secondary-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-button-secondary-border: rgb(255 255 255 / 0.08); + --color-components-button-secondary-border-hover: rgb(255 255 255 / 0.12); + --color-components-button-secondary-border-disabled: rgb(255 255 255 / 0.05); --color-components-button-tertiary-text: #d9d9de; - --color-components-button-tertiary-text-disabled: #ffffff33; - --color-components-button-tertiary-bg: #ffffff14; - --color-components-button-tertiary-bg-hover: #ffffff1f; - --color-components-button-tertiary-bg-disabled: #ffffff08; + --color-components-button-tertiary-text-disabled: rgb(255 255 255 / 0.2); + --color-components-button-tertiary-bg: rgb(255 255 255 / 0.08); + --color-components-button-tertiary-bg-hover: rgb(255 255 255 / 0.12); + --color-components-button-tertiary-bg-disabled: rgb(255 255 255 / 0.03); --color-components-button-ghost-text: #d9d9de; - --color-components-button-ghost-text-disabled: #ffffff33; - --color-components-button-ghost-bg-hover: #c8ceda14; + --color-components-button-ghost-text-disabled: rgb(255 255 255 / 0.2); + --color-components-button-ghost-bg-hover: rgb(200 206 218 / 0.08); - --color-components-button-destructive-primary-text: #fffffff2; - --color-components-button-destructive-primary-text-disabled: #ffffff33; + --color-components-button-destructive-primary-text: rgb(255 255 255 / 0.95); + --color-components-button-destructive-primary-text-disabled: rgb(255 255 255 / 0.2); --color-components-button-destructive-primary-bg: #d92d20; --color-components-button-destructive-primary-bg-hover: #f04438; - --color-components-button-destructive-primary-bg-disabled: #f0443824; - --color-components-button-destructive-primary-border: #ffffff1f; - --color-components-button-destructive-primary-border-hover: #ffffff33; - --color-components-button-destructive-primary-border-disabled: #ffffff14; + --color-components-button-destructive-primary-bg-disabled: rgb(240 68 56 / 0.14); + --color-components-button-destructive-primary-border: rgb(255 255 255 / 0.12); + --color-components-button-destructive-primary-border-hover: rgb(255 255 255 / 0.2); + --color-components-button-destructive-primary-border-disabled: rgb(255 255 255 / 0.08); --color-components-button-destructive-secondary-text: #f97066; - --color-components-button-destructive-secondary-text-disabled: #f0443833; - --color-components-button-destructive-secondary-bg: #ffffff1f; - --color-components-button-destructive-secondary-bg-hover: #f0443824; - --color-components-button-destructive-secondary-bg-disabled: #f0443814; - --color-components-button-destructive-secondary-border: #ffffff14; - --color-components-button-destructive-secondary-border-hover: #ffffff1f; - --color-components-button-destructive-secondary-border-disabled: #f0443814; + --color-components-button-destructive-secondary-text-disabled: rgb(240 68 56 / 0.2); + --color-components-button-destructive-secondary-bg: rgb(255 255 255 / 0.12); + --color-components-button-destructive-secondary-bg-hover: rgb(240 68 56 / 0.14); + --color-components-button-destructive-secondary-bg-disabled: rgb(240 68 56 / 0.08); + --color-components-button-destructive-secondary-border: rgb(255 255 255 / 0.08); + --color-components-button-destructive-secondary-border-hover: rgb(255 255 255 / 0.12); + --color-components-button-destructive-secondary-border-disabled: rgb(240 68 56 / 0.08); --color-components-button-destructive-tertiary-text: #f97066; - --color-components-button-destructive-tertiary-text-disabled: #f0443833; - --color-components-button-destructive-tertiary-bg: #f0443824; - --color-components-button-destructive-tertiary-bg-hover: #f0443840; - --color-components-button-destructive-tertiary-bg-disabled: #f0443814; + --color-components-button-destructive-tertiary-text-disabled: rgb(240 68 56 / 0.2); + --color-components-button-destructive-tertiary-bg: rgb(240 68 56 / 0.14); + --color-components-button-destructive-tertiary-bg-hover: rgb(240 68 56 / 0.25); + --color-components-button-destructive-tertiary-bg-disabled: rgb(240 68 56 / 0.08); --color-components-button-destructive-ghost-text: #f97066; - --color-components-button-destructive-ghost-text-disabled: #f0443833; - --color-components-button-destructive-ghost-bg-hover: #f0443824; - - --color-components-button-secondary-accent-text: #ffffffcc; - --color-components-button-secondary-accent-text-disabled: #ffffff33; - --color-components-button-secondary-accent-bg: #ffffff0d; - --color-components-button-secondary-accent-bg-hover: #ffffff14; - --color-components-button-secondary-accent-bg-disabled: #ffffff08; - --color-components-button-secondary-accent-border: #ffffff14; - --color-components-button-secondary-accent-border-hover: #ffffff1f; - --color-components-button-secondary-accent-border-disabled: #ffffff0d; + --color-components-button-destructive-ghost-text-disabled: rgb(240 68 56 / 0.2); + --color-components-button-destructive-ghost-bg-hover: rgb(240 68 56 / 0.14); + + --color-components-button-secondary-accent-text: rgb(255 255 255 / 0.8); + --color-components-button-secondary-accent-text-disabled: rgb(255 255 255 / 0.2); + --color-components-button-secondary-accent-bg: rgb(255 255 255 / 0.05); + --color-components-button-secondary-accent-bg-hover: rgb(255 255 255 / 0.08); + --color-components-button-secondary-accent-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-button-secondary-accent-border: rgb(255 255 255 / 0.08); + --color-components-button-secondary-accent-border-hover: rgb(255 255 255 / 0.12); + --color-components-button-secondary-accent-border-disabled: rgb(255 255 255 / 0.05); --color-components-button-indigo-bg: #444ce7; --color-components-button-indigo-bg-hover: #6172f3; - --color-components-button-indigo-bg-disabled: #ffffff08; + --color-components-button-indigo-bg-disabled: rgb(255 255 255 / 0.03); - --color-components-checkbox-icon: #fffffff2; - --color-components-checkbox-icon-disabled: #ffffff33; + --color-components-checkbox-icon: rgb(255 255 255 / 0.95); + --color-components-checkbox-icon-disabled: rgb(255 255 255 / 0.2); --color-components-checkbox-bg: #296dff; --color-components-checkbox-bg-hover: #5289ff; - --color-components-checkbox-bg-disabled: #ffffff08; - --color-components-checkbox-border: #ffffff66; - --color-components-checkbox-border-hover: #ffffff99; - --color-components-checkbox-border-disabled: #ffffff03; - --color-components-checkbox-bg-unchecked: #ffffff08; - --color-components-checkbox-bg-unchecked-hover: #ffffff0d; - --color-components-checkbox-bg-disabled-checked: #155aef33; + --color-components-checkbox-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-checkbox-border: rgb(255 255 255 / 0.4); + --color-components-checkbox-border-hover: rgb(255 255 255 / 0.6); + --color-components-checkbox-border-disabled: rgb(255 255 255 / 0.01); + --color-components-checkbox-bg-unchecked: rgb(255 255 255 / 0.03); + --color-components-checkbox-bg-unchecked-hover: rgb(255 255 255 / 0.05); + --color-components-checkbox-bg-disabled-checked: rgb(21 90 239 / 0.2); --color-components-radio-border-checked: #296dff; --color-components-radio-border-checked-hover: #5289ff; - --color-components-radio-border-checked-disabled: #155aef33; - --color-components-radio-bg-disabled: #ffffff08; - --color-components-radio-border: #ffffff66; - --color-components-radio-border-hover: #ffffff99; - --color-components-radio-border-disabled: #ffffff03; - --color-components-radio-bg: #ffffff00; - --color-components-radio-bg-hover: #ffffff0d; + --color-components-radio-border-checked-disabled: rgb(21 90 239 / 0.2); + --color-components-radio-bg-disabled: rgb(255 255 255 / 0.03); + --color-components-radio-border: rgb(255 255 255 / 0.4); + --color-components-radio-border-hover: rgb(255 255 255 / 0.6); + --color-components-radio-border-disabled: rgb(255 255 255 / 0.01); + --color-components-radio-bg: rgb(255 255 255 / 0); + --color-components-radio-bg-hover: rgb(255 255 255 / 0.05); --color-components-toggle-knob: #f4f4f5; - --color-components-toggle-knob-disabled: #ffffff33; + --color-components-toggle-knob-disabled: rgb(255 255 255 / 0.2); --color-components-toggle-bg: #296dff; --color-components-toggle-bg-hover: #5289ff; - --color-components-toggle-bg-disabled: #ffffff14; - --color-components-toggle-bg-unchecked: #ffffff33; - --color-components-toggle-bg-unchecked-hover: #ffffff4d; - --color-components-toggle-bg-unchecked-disabled: #ffffff14; + --color-components-toggle-bg-disabled: rgb(255 255 255 / 0.08); + --color-components-toggle-bg-unchecked: rgb(255 255 255 / 0.2); + --color-components-toggle-bg-unchecked-hover: rgb(255 255 255 / 0.3); + --color-components-toggle-bg-unchecked-disabled: rgb(255 255 255 / 0.08); --color-components-toggle-knob-hover: #fefefe; --color-components-card-bg: #222225; - --color-components-card-border: #ffffff08; + --color-components-card-border: rgb(255 255 255 / 0.03); --color-components-card-bg-alt: #27272b; + --color-components-card-bg-transparent: rgb(34 34 37 / 0); + --color-components-card-bg-alt-transparent: rgb(39 39 43 / 0); - --color-components-menu-item-text: #c8ceda99; - --color-components-menu-item-text-active: #fffffff2; - --color-components-menu-item-text-hover: #c8cedacc; - --color-components-menu-item-text-active-accent: #fffffff2; + --color-components-menu-item-text: rgb(200 206 218 / 0.6); + --color-components-menu-item-text-active: rgb(255 255 255 / 0.95); + --color-components-menu-item-text-hover: rgb(200 206 218 / 0.8); + --color-components-menu-item-text-active-accent: rgb(255 255 255 / 0.95); + --color-components-menu-item-bg-active: rgb(200 206 218 / 0.14); + --color-components-menu-item-bg-hover: rgb(200 206 218 / 0.08); --color-components-panel-bg: #222225; - --color-components-panel-bg-blur: #2c2c30f2; - --color-components-panel-border: #c8ceda24; - --color-components-panel-border-subtle: #c8ceda14; + --color-components-panel-bg-blur: rgb(44 44 48 / 0.95); + --color-components-panel-border: rgb(200 206 218 / 0.14); + --color-components-panel-border-subtle: rgb(200 206 218 / 0.08); --color-components-panel-gradient-2: #222225; --color-components-panel-gradient-1: #27272b; --color-components-panel-bg-alt: #222225; --color-components-panel-on-panel-item-bg: #27272b; --color-components-panel-on-panel-item-bg-hover: #3a3a40; --color-components-panel-on-panel-item-bg-alt: #3a3a40; - --color-components-panel-on-panel-item-bg-transparent: #2c2c30f2; - --color-components-panel-on-panel-item-bg-hover-transparent: #3a3a4000; - --color-components-panel-on-panel-item-bg-destructive-hover-transparent: #fffbfa00; + --color-components-panel-on-panel-item-bg-transparent: rgb(44 44 48 / 0.95); + --color-components-panel-on-panel-item-bg-hover-transparent: rgb(58 58 64 / 0); + --color-components-panel-on-panel-item-bg-destructive-hover-transparent: rgb(255 251 250 / 0); - --color-components-panel-bg-transparent: #22222500; + --color-components-panel-bg-transparent: rgb(34 34 37 / 0); - --color-components-main-nav-nav-button-text: #c8ceda99; + --color-components-main-nav-nav-button-text: rgb(200 206 218 / 0.6); --color-components-main-nav-nav-button-text-active: #f4f4f5; - --color-components-main-nav-nav-button-bg: #ffffff00; - --color-components-main-nav-nav-button-bg-active: #c8ceda24; - --color-components-main-nav-nav-button-border: #ffffff14; - --color-components-main-nav-nav-button-bg-hover: #c8ceda0a; + --color-components-main-nav-nav-button-bg: rgb(255 255 255 / 0); + --color-components-main-nav-nav-button-bg-active: rgb(200 206 218 / 0.14); + --color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.08); + --color-components-main-nav-nav-button-bg-hover: rgb(200 206 218 / 0.04); - --color-components-main-nav-nav-user-border: #ffffff0d; + --color-components-main-nav-nav-user-border: rgb(255 255 255 / 0.05); --color-components-slider-knob: #f4f4f5; --color-components-slider-knob-hover: #fefefe; - --color-components-slider-knob-disabled: #ffffff33; + --color-components-slider-knob-disabled: rgb(255 255 255 / 0.2); --color-components-slider-range: #296dff; - --color-components-slider-track: #ffffff33; - --color-components-slider-knob-border-hover: #1018284d; - --color-components-slider-knob-border: #10182833; - - --color-components-segmented-control-item-active-bg: #ffffff14; - --color-components-segmented-control-item-active-border: #c8ceda14; - --color-components-segmented-control-bg-normal: #18181bb3; - --color-components-segmented-control-item-active-accent-bg: #155aef33; - --color-components-segmented-control-item-active-accent-border: #155aef4d; - - --color-components-option-card-option-bg: #c8ceda0a; - --color-components-option-card-option-selected-bg: #ffffff0d; + --color-components-slider-track: rgb(255 255 255 / 0.2); + --color-components-slider-knob-border-hover: rgb(16 24 40 / 0.3); + --color-components-slider-knob-border: rgb(16 24 40 / 0.2); + + --color-components-segmented-control-item-active-bg: rgb(255 255 255 / 0.08); + --color-components-segmented-control-item-active-border: rgb(200 206 218 / 0.08); + --color-components-segmented-control-bg-normal: rgb(24 24 27 / 0.7); + --color-components-segmented-control-item-active-accent-bg: rgb(21 90 239 / 0.2); + --color-components-segmented-control-item-active-accent-border: rgb(21 90 239 / 0.3); + + --color-components-option-card-option-bg: rgb(200 206 218 / 0.04); + --color-components-option-card-option-selected-bg: rgb(255 255 255 / 0.05); --color-components-option-card-option-selected-border: #5289ff; - --color-components-option-card-option-border: #c8ceda33; - --color-components-option-card-option-bg-hover: #c8ceda24; - --color-components-option-card-option-border-hover: #c8ceda4d; + --color-components-option-card-option-border: rgb(200 206 218 / 0.2); + --color-components-option-card-option-bg-hover: rgb(200 206 218 / 0.14); + --color-components-option-card-option-border-hover: rgb(200 206 218 / 0.3); --color-components-tab-active: #296dff; - --color-components-badge-white-to-dark: #18181bcc; + --color-components-badge-white-to-dark: rgb(24 24 27 / 0.8); --color-components-badge-status-light-success-bg: #17b26a; --color-components-badge-status-light-success-border-inner: #47cd89; - --color-components-badge-status-light-success-halo: #17b26a4d; + --color-components-badge-status-light-success-halo: rgb(23 178 106 / 0.3); --color-components-badge-status-light-border-outer: #222225; - --color-components-badge-status-light-high-light: #ffffff4d; + --color-components-badge-status-light-high-light: rgb(255 255 255 / 0.3); --color-components-badge-status-light-warning-bg: #f79009; --color-components-badge-status-light-warning-border-inner: #fdb022; - --color-components-badge-status-light-warning-halo: #f790094d; + --color-components-badge-status-light-warning-halo: rgb(247 144 9 / 0.3); --color-components-badge-status-light-error-bg: #f04438; --color-components-badge-status-light-error-border-inner: #f97066; - --color-components-badge-status-light-error-halo: #f044384d; + --color-components-badge-status-light-error-halo: rgb(240 68 56 / 0.3); --color-components-badge-status-light-normal-bg: #0ba5ec; --color-components-badge-status-light-normal-border-inner: #36bffa; - --color-components-badge-status-light-normal-halo: #0ba5ec4d; + --color-components-badge-status-light-normal-halo: rgb(11 165 236 / 0.3); --color-components-badge-status-light-disabled-bg: #676f83; --color-components-badge-status-light-disabled-border-inner: #98a2b2; - --color-components-badge-status-light-disabled-halo: #c8ceda14; + --color-components-badge-status-light-disabled-halo: rgb(200 206 218 / 0.08); - --color-components-badge-bg-green-soft: #17b26a24; - --color-components-badge-bg-orange-soft: #f7900924; - --color-components-badge-bg-red-soft: #f0443824; - --color-components-badge-bg-blue-light-soft: #0ba5ec24; - --color-components-badge-bg-gray-soft: #c8ceda14; - --color-components-badge-bg-dimm: #ffffff08; + --color-components-badge-bg-green-soft: rgb(23 178 106 / 0.14); + --color-components-badge-bg-orange-soft: rgb(247 144 9 / 0.14); + --color-components-badge-bg-red-soft: rgb(240 68 56 / 0.14); + --color-components-badge-bg-blue-light-soft: rgb(11 165 236 / 0.14); + --color-components-badge-bg-gray-soft: rgb(200 206 218 / 0.08); + --color-components-badge-bg-dimm: rgb(255 255 255 / 0.03); --color-components-chart-line: #5289ff; - --color-components-chart-area-1: #155aef33; - --color-components-chart-area-2: #155aef0a; + --color-components-chart-area-1: rgb(21 90 239 / 0.2); + --color-components-chart-area-2: rgb(21 90 239 / 0.04); --color-components-chart-current-1: #5289ff; - --color-components-chart-current-2: #155aef4d; - --color-components-chart-bg: #18181bf2; + --color-components-chart-current-2: rgb(21 90 239 / 0.3); + --color-components-chart-bg: rgb(24 24 27 / 0.95); --color-components-actionbar-bg: #222225; - --color-components-actionbar-border: #c8ceda14; + --color-components-actionbar-border: rgb(200 206 218 / 0.08); --color-components-actionbar-bg-accent: #27272b; --color-components-actionbar-border-accent: #5289ff; - --color-components-dropzone-bg-alt: #18181bcc; - --color-components-dropzone-bg: #18181b66; - --color-components-dropzone-bg-accent: #155aef33; - --color-components-dropzone-border: #c8ceda24; - --color-components-dropzone-border-alt: #c8ceda33; + --color-components-dropzone-bg-alt: rgb(24 24 27 / 0.8); + --color-components-dropzone-bg: rgb(24 24 27 / 0.4); + --color-components-dropzone-bg-accent: rgb(21 90 239 / 0.2); + --color-components-dropzone-border: rgb(200 206 218 / 0.14); + --color-components-dropzone-border-alt: rgb(200 206 218 / 0.2); --color-components-dropzone-border-accent: #84abff; --color-components-progress-brand-progress: #5289ff; --color-components-progress-brand-border: #5289ff; - --color-components-progress-brand-bg: #155aef0a; + --color-components-progress-brand-bg: rgb(21 90 239 / 0.04); --color-components-progress-white-progress: #ffffff; - --color-components-progress-white-border: #fffffff2; - --color-components-progress-white-bg: #ffffff03; + --color-components-progress-white-border: rgb(255 255 255 / 0.95); + --color-components-progress-white-bg: rgb(255 255 255 / 0.01); --color-components-progress-gray-progress: #98a2b2; --color-components-progress-gray-border: #98a2b2; - --color-components-progress-gray-bg: #c8ceda05; + --color-components-progress-gray-bg: rgb(200 206 218 / 0.02); --color-components-progress-warning-progress: #fdb022; --color-components-progress-warning-border: #fdb022; - --color-components-progress-warning-bg: #f790090a; + --color-components-progress-warning-bg: rgb(247 144 9 / 0.04); --color-components-progress-error-progress: #f97066; --color-components-progress-error-border: #f97066; - --color-components-progress-error-bg: #f044380a; + --color-components-progress-error-bg: rgb(240 68 56 / 0.04); - --color-components-chat-input-audio-bg: #155aef33; - --color-components-chat-input-audio-wave-default: #c8ceda24; - --color-components-chat-input-bg-mask-1: #18181b0a; - --color-components-chat-input-bg-mask-2: #18181b99; - --color-components-chat-input-border: #c8ceda33; + --color-components-chat-input-audio-bg: rgb(21 90 239 / 0.2); + --color-components-chat-input-audio-wave-default: rgb(200 206 218 / 0.14); + --color-components-chat-input-bg-mask-1: rgb(24 24 27 / 0.04); + --color-components-chat-input-bg-mask-2: rgb(24 24 27 / 0.6); + --color-components-chat-input-border: rgb(200 206 218 / 0.2); --color-components-chat-input-audio-wave-active: #84abff; - --color-components-chat-input-audio-bg-alt: #18181be6; + --color-components-chat-input-audio-bg-alt: rgb(24 24 27 / 0.9); - --color-components-avatar-shape-fill-stop-0: #fffffff2; - --color-components-avatar-shape-fill-stop-100: #ffffffcc; + --color-components-avatar-shape-fill-stop-0: rgb(255 255 255 / 0.95); + --color-components-avatar-shape-fill-stop-100: rgb(255 255 255 / 0.8); - --color-components-avatar-bg-mask-stop-0: #ffffff33; - --color-components-avatar-bg-mask-stop-100: #ffffff08; + --color-components-avatar-bg-mask-stop-0: rgb(255 255 255 / 0.2); + --color-components-avatar-bg-mask-stop-100: rgb(255 255 255 / 0.03); --color-components-avatar-default-avatar-bg: #222225; - --color-components-avatar-mask-darkmode-dimmed: #0000001f; + --color-components-avatar-mask-darkmode-dimmed: rgb(0 0 0 / 0.12); - --color-components-label-gray: #c8ceda24; + --color-components-label-gray: rgb(200 206 218 / 0.14); --color-components-premium-badge-blue-bg-stop-0: #5289ff; --color-components-premium-badge-blue-bg-stop-100: #296dff; - --color-components-premium-badge-blue-stroke-stop-0: #ffffff33; + --color-components-premium-badge-blue-stroke-stop-0: rgb(255 255 255 / 0.2); --color-components-premium-badge-blue-stroke-stop-100: #296dff; --color-components-premium-badge-blue-text-stop-0: #eff4ff; --color-components-premium-badge-blue-text-stop-100: #b2caff; @@ -276,14 +280,14 @@ html[data-theme="dark"] { --color-components-premium-badge-blue-bg-stop-0-hover: #84abff; --color-components-premium-badge-blue-bg-stop-100-hover: #004aeb; --color-components-premium-badge-blue-glow-hover: #d1e0ff; - --color-components-premium-badge-blue-stroke-stop-0-hover: #ffffff80; + --color-components-premium-badge-blue-stroke-stop-0-hover: rgb(255 255 255 / 0.5); --color-components-premium-badge-blue-stroke-stop-100-hover: #296dff; - --color-components-premium-badge-highlight-stop-0: #ffffff1f; - --color-components-premium-badge-highlight-stop-100: #ffffff33; + --color-components-premium-badge-highlight-stop-0: rgb(255 255 255 / 0.12); + --color-components-premium-badge-highlight-stop-100: rgb(255 255 255 / 0.2); --color-components-premium-badge-indigo-bg-stop-0: #6172f3; --color-components-premium-badge-indigo-bg-stop-100: #3538cd; - --color-components-premium-badge-indigo-stroke-stop-0: #ffffff33; + --color-components-premium-badge-indigo-stroke-stop-0: rgb(255 255 255 / 0.2); --color-components-premium-badge-indigo-stroke-stop-100: #444ce7; --color-components-premium-badge-indigo-text-stop-0: #eef4ff; --color-components-premium-badge-indigo-text-stop-100: #c7d7fe; @@ -291,12 +295,12 @@ html[data-theme="dark"] { --color-components-premium-badge-indigo-glow-hover: #e0eaff; --color-components-premium-badge-indigo-bg-stop-0-hover: #a4bcfd; --color-components-premium-badge-indigo-bg-stop-100-hover: #3538cd; - --color-components-premium-badge-indigo-stroke-stop-0-hover: #ffffff80; + --color-components-premium-badge-indigo-stroke-stop-0-hover: rgb(255 255 255 / 0.5); --color-components-premium-badge-indigo-stroke-stop-100-hover: #444ce7; --color-components-premium-badge-grey-bg-stop-0: #676f83; --color-components-premium-badge-grey-bg-stop-100: #495464; - --color-components-premium-badge-grey-stroke-stop-0: #ffffff1f; + --color-components-premium-badge-grey-stroke-stop-0: rgb(255 255 255 / 0.12); --color-components-premium-badge-grey-stroke-stop-100: #495464; --color-components-premium-badge-grey-text-stop-0: #f9fafb; --color-components-premium-badge-grey-text-stop-100: #e9ebf0; @@ -304,12 +308,12 @@ html[data-theme="dark"] { --color-components-premium-badge-grey-glow-hover: #f2f4f7; --color-components-premium-badge-grey-bg-stop-0-hover: #98a2b2; --color-components-premium-badge-grey-bg-stop-100-hover: #354052; - --color-components-premium-badge-grey-stroke-stop-0-hover: #ffffff80; + --color-components-premium-badge-grey-stroke-stop-0-hover: rgb(255 255 255 / 0.5); --color-components-premium-badge-grey-stroke-stop-100-hover: #676f83; --color-components-premium-badge-orange-bg-stop-0: #ff692e; --color-components-premium-badge-orange-bg-stop-100: #e04f16; - --color-components-premium-badge-orange-stroke-stop-0: #ffffff33; + --color-components-premium-badge-orange-stroke-stop-0: rgb(255 255 255 / 0.2); --color-components-premium-badge-orange-stroke-stop-100: #ff4405; --color-components-premium-badge-orange-text-stop-0: #fef6ee; --color-components-premium-badge-orange-text-stop-100: #f9dbaf; @@ -317,14 +321,14 @@ html[data-theme="dark"] { --color-components-premium-badge-orange-glow-hover: #fdead7; --color-components-premium-badge-orange-bg-stop-0-hover: #ff692e; --color-components-premium-badge-orange-bg-stop-100-hover: #b93815; - --color-components-premium-badge-orange-stroke-stop-0-hover: #ffffff80; + --color-components-premium-badge-orange-stroke-stop-0-hover: rgb(255 255 255 / 0.5); --color-components-premium-badge-orange-stroke-stop-100-hover: #ff4405; - --color-components-progress-bar-bg: #c8ceda14; - --color-components-progress-bar-progress: #c8ceda24; - --color-components-progress-bar-border: #ffffff08; - --color-components-progress-bar-progress-solid: #fffffff2; - --color-components-progress-bar-progress-highlight: #c8ceda33; + --color-components-progress-bar-bg: rgb(200 206 218 / 0.08); + --color-components-progress-bar-progress: rgb(200 206 218 / 0.14); + --color-components-progress-bar-border: rgb(255 255 255 / 0.03); + --color-components-progress-bar-progress-solid: rgb(255 255 255 / 0.95); + --color-components-progress-bar-progress-highlight: rgb(200 206 218 / 0.2); --color-components-icon-bg-red-solid: #d92d20; --color-components-icon-bg-rose-solid: #e31b54; @@ -338,25 +342,25 @@ html[data-theme="dark"] { --color-components-icon-bg-indigo-solid: #444ce7; --color-components-icon-bg-violet-solid: #7839ee; --color-components-icon-bg-midnight-solid: #5d698d; - --color-components-icon-bg-rose-soft: #f63d6833; - --color-components-icon-bg-pink-soft: #ee46bc33; - --color-components-icon-bg-orange-dark-soft: #ff440533; - --color-components-icon-bg-yellow-soft: #eaaa0833; - --color-components-icon-bg-green-soft: #66c61c33; - --color-components-icon-bg-teal-soft: #15b79e33; - --color-components-icon-bg-blue-light-soft: #0ba5ec33; - --color-components-icon-bg-blue-soft: #155aef33; - --color-components-icon-bg-indigo-soft: #6172f333; - --color-components-icon-bg-violet-soft: #875bf733; - --color-components-icon-bg-midnight-soft: #828dad33; - --color-components-icon-bg-red-soft: #f0443833; + --color-components-icon-bg-rose-soft: rgb(246 61 104 / 0.2); + --color-components-icon-bg-pink-soft: rgb(238 70 188 / 0.2); + --color-components-icon-bg-orange-dark-soft: rgb(255 68 5 / 0.2); + --color-components-icon-bg-yellow-soft: rgb(234 170 8 / 0.2); + --color-components-icon-bg-green-soft: rgb(102 198 28 / 0.2); + --color-components-icon-bg-teal-soft: rgb(21 183 158 / 0.2); + --color-components-icon-bg-blue-light-soft: rgb(11 165 236 / 0.2); + --color-components-icon-bg-blue-soft: rgb(21 90 239 / 0.2); + --color-components-icon-bg-indigo-soft: rgb(97 114 243 / 0.2); + --color-components-icon-bg-violet-soft: rgb(135 91 247 / 0.2); + --color-components-icon-bg-midnight-soft: rgb(130 141 173 / 0.2); + --color-components-icon-bg-red-soft: rgb(240 68 56 / 0.2); --color-components-icon-bg-orange-solid: #f79009; - --color-components-icon-bg-orange-soft: #f7900933; + --color-components-icon-bg-orange-soft: rgb(247 144 9 / 0.2); --color-text-primary: #fbfbfc; --color-text-secondary: #d9d9de; - --color-text-tertiary: #c8ceda99; - --color-text-quaternary: #c8ceda66; + --color-text-tertiary: rgb(200 206 218 / 0.6); + --color-text-quaternary: rgb(200 206 218 / 0.4); --color-text-destructive: #f97066; --color-text-success: #17b26a; --color-text-warning: #f79009; @@ -364,80 +368,85 @@ html[data-theme="dark"] { --color-text-success-secondary: #47cd89; --color-text-warning-secondary: #fdb022; --color-text-accent: #5289ff; - --color-text-primary-on-surface: #fffffff2; - --color-text-placeholder: #c8ceda4d; - --color-text-disabled: #c8ceda4d; + --color-text-primary-on-surface: rgb(255 255 255 / 0.95); + --color-text-placeholder: rgb(200 206 218 / 0.3); + --color-text-disabled: rgb(200 206 218 / 0.3); --color-text-accent-secondary: #84abff; --color-text-accent-light-mode-only: #d9d9de; - --color-text-text-selected: #155aef4d; - --color-text-secondary-on-surface: #ffffffe6; + --color-text-text-selected: rgb(21 90 239 / 0.3); + --color-text-secondary-on-surface: rgb(255 255 255 / 0.9); --color-text-logo-text: #e9e9ec; - --color-text-empty-state-icon: #c8ceda4d; + --color-text-empty-state-icon: rgb(200 206 218 / 0.3); --color-text-inverted: #ffffff; - --color-text-inverted-dimmed: #ffffffcc; + --color-text-inverted-dimmed: rgb(255 255 255 / 0.8); --color-background-body: #1d1d20; --color-background-default-subtle: #222225; --color-background-neutral-subtle: #1d1d20; - --color-background-sidenav-bg: #27272aeb; + --color-background-sidenav-bg: rgb(39 39 42 / 0.92); --color-background-default: #222225; - --color-background-soft: #18181b40; + --color-background-soft: rgb(24 24 27 / 0.25); --color-background-gradient-bg-fill-chat-bg-1: #222225; --color-background-gradient-bg-fill-chat-bg-2: #1d1d20; - --color-background-gradient-bg-fill-chat-bubble-bg-1: #c8ceda14; - --color-background-gradient-bg-fill-chat-bubble-bg-2: #c8ceda05; - --color-background-gradient-bg-fill-chat-bubble-bg-3: #a5bddb; - --color-background-gradient-bg-fill-debug-bg-1: #c8ceda14; - --color-background-gradient-bg-fill-debug-bg-2: #18181b0a; - - --color-background-gradient-mask-gray: #18181b14; - --color-background-gradient-mask-transparent: #00000000; - --color-background-gradient-mask-input-clear-2: #393a3e00; + --color-background-gradient-bg-fill-chat-bubble-bg-1: rgb(200 206 218 / 0.08); + --color-background-gradient-bg-fill-chat-bubble-bg-2: rgb(200 206 218 / 0.02); + --color-background-gradient-bg-fill-debug-bg-1: rgb(200 206 218 / 0.08); + --color-background-gradient-bg-fill-debug-bg-2: rgb(24 24 27 / 0.04); + + --color-background-gradient-mask-gray: rgb(24 24 27 / 0.08); + --color-background-gradient-mask-transparent: rgb(0 0 0 / 0); + --color-background-gradient-mask-input-clear-2: rgb(57 58 62 / 0); --color-background-gradient-mask-input-clear-1: #393a3e; - --color-background-gradient-mask-transparent-dark: #00000000; - --color-background-gradient-mask-side-panel-2: #18181be6; - --color-background-gradient-mask-side-panel-1: #18181b0a; + --color-background-gradient-mask-transparent-dark: rgb(0 0 0 / 0); + --color-background-gradient-mask-side-panel-2: rgb(24 24 27 / 0.9); + --color-background-gradient-mask-side-panel-1: rgb(24 24 27 / 0.04); --color-background-default-burn: #1d1d20; - --color-background-overlay-fullscreen: #27272af7; - --color-background-default-lighter: #c8ceda0a; - --color-background-section: #18181b66; - --color-background-interaction-from-bg-1: #18181b66; - --color-background-interaction-from-bg-2: #18181b24; - --color-background-section-burn: #18181b99; + --color-background-overlay-fullscreen: rgb(39 39 42 / 0.97); + --color-background-default-lighter: rgb(200 206 218 / 0.04); + --color-background-section: rgb(24 24 27 / 0.4); + --color-background-interaction-from-bg-1: rgb(24 24 27 / 0.4); + --color-background-interaction-from-bg-2: rgb(24 24 27 / 0.14); + --color-background-section-burn: rgb(24 24 27 / 0.6); --color-background-default-dodge: #3a3a40; - --color-background-overlay: #18181bcc; + --color-background-overlay: rgb(24 24 27 / 0.8); --color-background-default-dimmed: #27272b; --color-background-default-hover: #27272b; - --color-background-overlay-alt: #18181b66; - --color-background-surface-white: #ffffffe6; - --color-background-overlay-destructive: #f044384d; - --color-background-overlay-backdrop: #18181bf2; - - --color-shadow-shadow-1: #0000000d; - --color-shadow-shadow-3: #0000001a; - --color-shadow-shadow-4: #0000001f; - --color-shadow-shadow-5: #00000029; - --color-shadow-shadow-6: #00000033; - --color-shadow-shadow-7: #0000003d; - --color-shadow-shadow-8: #00000047; - --color-shadow-shadow-9: #0000005c; - --color-shadow-shadow-2: #00000014; - --color-shadow-shadow-10: #00000066; - - --color-workflow-block-border: #ffffff14; - --color-workflow-block-parma-bg: #ffffff0d; + --color-background-overlay-alt: rgb(24 24 27 / 0.4); + --color-background-surface-white: rgb(255 255 255 / 0.9); + --color-background-overlay-destructive: rgb(240 68 56 / 0.3); + --color-background-overlay-backdrop: rgb(24 24 27 / 0.95); + --color-background-body-transparent: rgb(29 29 32 / 0); + + --color-shadow-shadow-1: rgb(0 0 0 / 0.05); + --color-shadow-shadow-3: rgb(0 0 0 / 0.1); + --color-shadow-shadow-4: rgb(0 0 0 / 0.12); + --color-shadow-shadow-5: rgb(0 0 0 / 0.16); + --color-shadow-shadow-6: rgb(0 0 0 / 0.2); + --color-shadow-shadow-7: rgb(0 0 0 / 0.24); + --color-shadow-shadow-8: rgb(0 0 0 / 0.28); + --color-shadow-shadow-9: rgb(0 0 0 / 0.36); + --color-shadow-shadow-2: rgb(0 0 0 / 0.08); + --color-shadow-shadow-10: rgb(0 0 0 / 0.4); + + --color-workflow-block-border: rgb(255 255 255 / 0.08); + --color-workflow-block-parma-bg: rgb(255 255 255 / 0.05); --color-workflow-block-bg: #27272b; - --color-workflow-block-bg-transparent: #27272bf5; - --color-workflow-block-border-highlight: #c8ceda33; + --color-workflow-block-bg-transparent: rgb(39 39 43 / 0.96); + --color-workflow-block-border-highlight: rgb(200 206 218 / 0.2); + --color-workflow-block-wrapper-bg-1: #27272b; + --color-workflow-block-wrapper-bg-2: rgb(39 39 43 / 0.2); - --color-workflow-canvas-workflow-dot-color: #8585ad1c; + --color-workflow-canvas-workflow-dot-color: rgb(133 133 173 / 0.11); --color-workflow-canvas-workflow-bg: #1d1d20; + --color-workflow-canvas-workflow-top-bar-1: #1d1d20; + --color-workflow-canvas-workflow-top-bar-2: rgb(29 29 32 / 0.08); + --color-workflow-canvas-canvas-overlay: rgb(29 29 32 / 0.8); --color-workflow-link-line-active: #5289ff; --color-workflow-link-line-normal: #676f83; --color-workflow-link-line-handle: #5289ff; - --color-workflow-link-line-normal-transparent: #676f8333; + --color-workflow-link-line-normal-transparent: rgb(103 111 131 / 0.2); --color-workflow-link-line-failure-active: #fdb022; --color-workflow-link-line-failure-handle: #fdb022; --color-workflow-link-line-failure-button-bg: #f79009; @@ -450,87 +459,90 @@ html[data-theme="dark"] { --color-workflow-link-line-error-handle: #f97066; --color-workflow-minimap-bg: #27272b; - --color-workflow-minimap-block: #c8ceda14; - - --color-workflow-display-success-bg: #17b26a33; - --color-workflow-display-success-border-1: #17b26ae6; - --color-workflow-display-success-border-2: #17b26acc; - --color-workflow-display-success-vignette-color: #17b26a40; - --color-workflow-display-success-bg-line-pattern: #18181bcc; - - --color-workflow-display-glass-1: #ffffff08; - --color-workflow-display-glass-2: #ffffff0d; - --color-workflow-display-vignette-dark: #00000066; - --color-workflow-display-highlight: #ffffff1f; - --color-workflow-display-outline: #18181bf2; - --color-workflow-display-error-bg: #f0443833; - --color-workflow-display-error-bg-line-pattern: #18181bcc; - --color-workflow-display-error-border-1: #f04438e6; - --color-workflow-display-error-border-2: #f04438cc; - --color-workflow-display-error-vignette-color: #f0443840; - - --color-workflow-display-warning-bg: #f7900933; - --color-workflow-display-warning-bg-line-pattern: #18181bcc; - --color-workflow-display-warning-border-1: #f79009e6; - --color-workflow-display-warning-border-2: #f79009cc; - --color-workflow-display-warning-vignette-color: #f7900940; - - --color-workflow-display-normal-bg: #0ba5ec33; - --color-workflow-display-normal-bg-line-pattern: #18181bcc; - --color-workflow-display-normal-border-1: #0ba5ece6; - --color-workflow-display-normal-border-2: #0ba5eccc; - --color-workflow-display-normal-vignette-color: #0ba5ec40; - - --color-workflow-display-disabled-bg: #c8ceda33; - --color-workflow-display-disabled-bg-line-pattern: #18181bcc; - --color-workflow-display-disabled-border-1: #c8ceda99; - --color-workflow-display-disabled-border-2: #c8ceda40; - --color-workflow-display-disabled-vignette-color: #c8ceda40; - --color-workflow-display-disabled-outline: #18181bf2; - - --color-workflow-workflow-progress-bg-1: #18181b40; - --color-workflow-workflow-progress-bg-2: #18181b0a; - - --color-divider-subtle: #c8ceda14; - --color-divider-regular: #c8ceda24; - --color-divider-deep: #c8ceda33; - --color-divider-burn: #18181bf2; - --color-divider-intense: #c8ceda66; + --color-workflow-minimap-block: rgb(200 206 218 / 0.08); + + --color-workflow-display-success-bg: rgb(23 178 106 / 0.2); + --color-workflow-display-success-border-1: rgb(23 178 106 / 0.9); + --color-workflow-display-success-border-2: rgb(23 178 106 / 0.8); + --color-workflow-display-success-vignette-color: rgb(23 178 106 / 0.25); + --color-workflow-display-success-bg-line-pattern: rgb(24 24 27 / 0.8); + + --color-workflow-display-glass-1: rgb(255 255 255 / 0.03); + --color-workflow-display-glass-2: rgb(255 255 255 / 0.05); + --color-workflow-display-vignette-dark: rgb(0 0 0 / 0.4); + --color-workflow-display-highlight: rgb(255 255 255 / 0.12); + --color-workflow-display-outline: rgb(24 24 27 / 0.95); + --color-workflow-display-error-bg: rgb(240 68 56 / 0.2); + --color-workflow-display-error-bg-line-pattern: rgb(24 24 27 / 0.8); + --color-workflow-display-error-border-1: rgb(240 68 56 / 0.9); + --color-workflow-display-error-border-2: rgb(240 68 56 / 0.8); + --color-workflow-display-error-vignette-color: rgb(240 68 56 / 0.25); + + --color-workflow-display-warning-bg: rgb(247 144 9 / 0.2); + --color-workflow-display-warning-bg-line-pattern: rgb(24 24 27 / 0.8); + --color-workflow-display-warning-border-1: rgb(247 144 9 / 0.9); + --color-workflow-display-warning-border-2: rgb(247 144 9 / 0.8); + --color-workflow-display-warning-vignette-color: rgb(247 144 9 / 0.25); + + --color-workflow-display-normal-bg: rgb(11 165 236 / 0.2); + --color-workflow-display-normal-bg-line-pattern: rgb(24 24 27 / 0.8); + --color-workflow-display-normal-border-1: rgb(11 165 236 / 0.9); + --color-workflow-display-normal-border-2: rgb(11 165 236 / 0.8); + --color-workflow-display-normal-vignette-color: rgb(11 165 236 / 0.25); + + --color-workflow-display-disabled-bg: rgb(200 206 218 / 0.2); + --color-workflow-display-disabled-bg-line-pattern: rgb(24 24 27 / 0.8); + --color-workflow-display-disabled-border-1: rgb(200 206 218 / 0.6); + --color-workflow-display-disabled-border-2: rgb(200 206 218 / 0.25); + --color-workflow-display-disabled-vignette-color: rgb(200 206 218 / 0.25); + --color-workflow-display-disabled-outline: rgb(24 24 27 / 0.95); + + --color-workflow-workflow-progress-bg-1: rgb(24 24 27 / 0.25); + --color-workflow-workflow-progress-bg-2: rgb(24 24 27 / 0.04); + + --color-divider-subtle: rgb(200 206 218 / 0.08); + --color-divider-regular: rgb(200 206 218 / 0.14); + --color-divider-deep: rgb(200 206 218 / 0.2); + --color-divider-burn: rgb(24 24 27 / 0.95); + --color-divider-intense: rgb(200 206 218 / 0.4); --color-divider-solid: #3a3a40; --color-divider-solid-alt: #747481; - --color-state-base-hover: #c8ceda14; - --color-state-base-active: #c8ceda33; - --color-state-base-hover-alt: #c8ceda24; - --color-state-base-handle: #c8ceda4d; - --color-state-base-handle-hover: #c8ceda80; - --color-state-base-hover-subtle: #c8ceda0a; + --color-state-base-hover: rgb(200 206 218 / 0.08); + --color-state-base-active: rgb(200 206 218 / 0.2); + --color-state-base-hover-alt: rgb(200 206 218 / 0.14); + --color-state-base-handle: rgb(200 206 218 / 0.3); + --color-state-base-handle-hover: rgb(200 206 218 / 0.5); + --color-state-base-hover-subtle: rgb(200 206 218 / 0.04); - --color-state-accent-hover: #155aef24; - --color-state-accent-active: #155aef24; - --color-state-accent-hover-alt: #155aef40; + --color-state-accent-hover: rgb(21 90 239 / 0.14); + --color-state-accent-active: rgb(21 90 239 / 0.14); + --color-state-accent-hover-alt: rgb(21 90 239 / 0.25); --color-state-accent-solid: #5289ff; - --color-state-accent-active-alt: #155aef33; + --color-state-accent-active-alt: rgb(21 90 239 / 0.2); - --color-state-destructive-hover: #f0443824; - --color-state-destructive-hover-alt: #f0443840; - --color-state-destructive-active: #f044384d; + --color-state-destructive-hover: rgb(240 68 56 / 0.14); + --color-state-destructive-hover-alt: rgb(240 68 56 / 0.25); + --color-state-destructive-active: rgb(240 68 56 / 0.3); --color-state-destructive-solid: #f97066; --color-state-destructive-border: #f97066; + --color-state-destructive-hover-transparent: rgb(240 68 56 / 0); - --color-state-success-hover: #17b26a24; - --color-state-success-hover-alt: #17b26a40; - --color-state-success-active: #17b26a4d; + --color-state-success-hover: rgb(23 178 106 / 0.14); + --color-state-success-hover-alt: rgb(23 178 106 / 0.25); + --color-state-success-active: rgb(23 178 106 / 0.3); --color-state-success-solid: #47cd89; - --color-state-warning-hover: #f7900924; - --color-state-warning-hover-alt: #f7900940; - --color-state-warning-active: #f790094d; + --color-state-warning-hover: rgb(247 144 9 / 0.14); + --color-state-warning-hover-alt: rgb(247 144 9 / 0.25); + --color-state-warning-active: rgb(247 144 9 / 0.3); --color-state-warning-solid: #f79009; + --color-state-warning-hover-transparent: rgb(247 144 9 / 0); - --color-effects-highlight: #c8ceda14; - --color-effects-highlight-lightmode-off: #c8ceda14; + --color-effects-highlight: rgb(200 206 218 / 0.08); + --color-effects-highlight-lightmode-off: rgb(200 206 218 / 0.08); --color-effects-image-frame: #ffffff; + --color-effects-icon-border: rgb(255 255 255 / 0.15); --color-util-colors-orange-dark-orange-dark-50: #57130a; --color-util-colors-orange-dark-orange-dark-100: #771a0d; @@ -549,7 +561,7 @@ html[data-theme="dark"] { --color-util-colors-orange-orange-500: #ef6820; --color-util-colors-orange-orange-600: #f38744; --color-util-colors-orange-orange-700: #f7b27a; - --color-util-colors-orange-orange-100-transparent: #77291700; + --color-util-colors-orange-orange-100-transparent: rgb(119 41 23 / 0); --color-util-colors-pink-pink-50: #4e0d30; --color-util-colors-pink-pink-100: #851651; @@ -725,16 +737,19 @@ html[data-theme="dark"] { --color-third-party-LangChain: #ffffff; --color-third-party-Langfuse: #ffffff; --color-third-party-Github: #ffffff; - --color-third-party-Github-tertiary: #c8ceda99; + --color-third-party-Github-tertiary: rgb(200 206 218 / 0.6); --color-third-party-Github-secondary: #d9d9de; --color-third-party-model-bg-openai: #121212; --color-third-party-model-bg-anthropic: #1d1917; - --color-third-party-model-bg-default: #0b0b0e; + --color-third-party-model-bg-default: #1d1d20; --color-third-party-aws: #141f2e; --color-third-party-aws-alt: #192639; --color-saas-background: #0b0b0e; - --color-saas-pricing-grid-bg: #c8ceda33; + --color-saas-pricing-grid-bg: rgb(200 206 218 / 0.2); + + --color-dify-logo-dify-logo-blue: #e8e8e8; + --color-dify-logo-dify-logo-black: #e8e8e8; } diff --git a/web/themes/light.css b/web/themes/light.css index 8a3f9d9d48..9a0a958bfd 100644 --- a/web/themes/light.css +++ b/web/themes/light.css @@ -1,79 +1,79 @@ /* Attention: Generate by code. Don't update by hand!!! */ html[data-theme="light"] { - --color-components-input-bg-normal: #c8ceda40; + --color-components-input-bg-normal: rgb(200 206 218 / 0.25); --color-components-input-text-placeholder: #98a2b2; - --color-components-input-bg-hover: #c8ceda24; + --color-components-input-bg-hover: rgb(200 206 218 / 0.14); --color-components-input-bg-active: #f9fafb; --color-components-input-border-active: #d0d5dc; --color-components-input-border-destructive: #fda29b; --color-components-input-text-filled: #101828; --color-components-input-bg-destructive: #ffffff; - --color-components-input-bg-disabled: #c8ceda24; + --color-components-input-bg-disabled: rgb(200 206 218 / 0.14); --color-components-input-text-disabled: #d0d5dc; --color-components-input-text-filled-disabled: #676f83; --color-components-input-border-hover: #d0d5dc; --color-components-input-border-active-prompt-1: #0ba5ec; --color-components-input-border-active-prompt-2: #155aef; - --color-components-kbd-bg-gray: #1018280a; - --color-components-kbd-bg-white: #ffffff1f; + --color-components-kbd-bg-gray: rgb(16 24 40 / 0.04); + --color-components-kbd-bg-white: rgb(255 255 255 / 0.12); - --color-components-tooltip-bg: #fffffff2; + --color-components-tooltip-bg: rgb(255 255 255 / 0.95); --color-components-button-primary-text: #ffffff; --color-components-button-primary-bg: #155aef; - --color-components-button-primary-border: #1018280a; + --color-components-button-primary-border: rgb(16 24 40 / 0.04); --color-components-button-primary-bg-hover: #004aeb; - --color-components-button-primary-border-hover: #10182814; - --color-components-button-primary-bg-disabled: #155aef24; - --color-components-button-primary-border-disabled: #ffffff00; - --color-components-button-primary-text-disabled: #ffffff99; + --color-components-button-primary-border-hover: rgb(16 24 40 / 0.08); + --color-components-button-primary-bg-disabled: rgb(21 90 239 / 0.14); + --color-components-button-primary-border-disabled: rgb(255 255 255 / 0); + --color-components-button-primary-text-disabled: rgb(255 255 255 / 0.6); --color-components-button-secondary-text: #354052; - --color-components-button-secondary-text-disabled: #10182840; + --color-components-button-secondary-text-disabled: rgb(16 24 40 / 0.25); --color-components-button-secondary-bg: #ffffff; --color-components-button-secondary-bg-hover: #f9fafb; --color-components-button-secondary-bg-disabled: #f9fafb; - --color-components-button-secondary-border: #10182824; - --color-components-button-secondary-border-hover: #10182833; - --color-components-button-secondary-border-disabled: #1018280a; + --color-components-button-secondary-border: rgb(16 24 40 / 0.14); + --color-components-button-secondary-border-hover: rgb(16 24 40 / 0.2); + --color-components-button-secondary-border-disabled: rgb(16 24 40 / 0.04); --color-components-button-tertiary-text: #354052; - --color-components-button-tertiary-text-disabled: #10182840; + --color-components-button-tertiary-text-disabled: rgb(16 24 40 / 0.25); --color-components-button-tertiary-bg: #f2f4f7; --color-components-button-tertiary-bg-hover: #e9ebf0; --color-components-button-tertiary-bg-disabled: #f9fafb; --color-components-button-ghost-text: #354052; - --color-components-button-ghost-text-disabled: #10182840; - --color-components-button-ghost-bg-hover: #c8ceda33; + --color-components-button-ghost-text-disabled: rgb(16 24 40 / 0.25); + --color-components-button-ghost-bg-hover: rgb(200 206 218 / 0.2); --color-components-button-destructive-primary-text: #ffffff; - --color-components-button-destructive-primary-text-disabled: #ffffff99; + --color-components-button-destructive-primary-text-disabled: rgb(255 255 255 / 0.6); --color-components-button-destructive-primary-bg: #d92d20; --color-components-button-destructive-primary-bg-hover: #b42318; --color-components-button-destructive-primary-bg-disabled: #fee4e2; - --color-components-button-destructive-primary-border: #18181b0a; - --color-components-button-destructive-primary-border-hover: #18181b14; - --color-components-button-destructive-primary-border-disabled: #ffffff00; + --color-components-button-destructive-primary-border: rgb(24 24 27 / 0.04); + --color-components-button-destructive-primary-border-hover: rgb(24 24 27 / 0.08); + --color-components-button-destructive-primary-border-disabled: rgb(255 255 255 / 0); --color-components-button-destructive-secondary-text: #d92d20; - --color-components-button-destructive-secondary-text-disabled: #f0443833; + --color-components-button-destructive-secondary-text-disabled: rgb(240 68 56 / 0.2); --color-components-button-destructive-secondary-bg: #ffffff; --color-components-button-destructive-secondary-bg-hover: #fef3f2; --color-components-button-destructive-secondary-bg-disabled: #fef3f2; - --color-components-button-destructive-secondary-border: #18181b14; - --color-components-button-destructive-secondary-border-hover: #f0443840; - --color-components-button-destructive-secondary-border-disabled: #f044380a; + --color-components-button-destructive-secondary-border: rgb(24 24 27 / 0.08); + --color-components-button-destructive-secondary-border-hover: rgb(240 68 56 / 0.25); + --color-components-button-destructive-secondary-border-disabled: rgb(240 68 56 / 0.04); --color-components-button-destructive-tertiary-text: #d92d20; - --color-components-button-destructive-tertiary-text-disabled: #f0443833; + --color-components-button-destructive-tertiary-text-disabled: rgb(240 68 56 / 0.2); --color-components-button-destructive-tertiary-bg: #fee4e2; --color-components-button-destructive-tertiary-bg-hover: #fecdca; - --color-components-button-destructive-tertiary-bg-disabled: #f044380a; + --color-components-button-destructive-tertiary-bg-disabled: rgb(240 68 56 / 0.04); --color-components-button-destructive-ghost-text: #d92d20; - --color-components-button-destructive-ghost-text-disabled: #f0443833; + --color-components-button-destructive-ghost-text-disabled: rgb(240 68 56 / 0.2); --color-components-button-destructive-ghost-bg-hover: #fee4e2; --color-components-button-secondary-accent-text: #155aef; @@ -81,22 +81,22 @@ html[data-theme="light"] { --color-components-button-secondary-accent-bg: #ffffff; --color-components-button-secondary-accent-bg-hover: #f2f4f7; --color-components-button-secondary-accent-bg-disabled: #f9fafb; - --color-components-button-secondary-accent-border: #10182824; - --color-components-button-secondary-accent-border-hover: #10182824; - --color-components-button-secondary-accent-border-disabled: #1018280a; + --color-components-button-secondary-accent-border: rgb(16 24 40 / 0.14); + --color-components-button-secondary-accent-border-hover: rgb(16 24 40 / 0.14); + --color-components-button-secondary-accent-border-disabled: rgb(16 24 40 / 0.04); --color-components-button-indigo-bg: #444ce7; --color-components-button-indigo-bg-hover: #3538cd; - --color-components-button-indigo-bg-disabled: #6172f324; + --color-components-button-indigo-bg-disabled: rgb(97 114 243 / 0.14); --color-components-checkbox-icon: #ffffff; - --color-components-checkbox-icon-disabled: #ffffff80; + --color-components-checkbox-icon-disabled: rgb(255 255 255 / 0.5); --color-components-checkbox-bg: #155aef; --color-components-checkbox-bg-hover: #004aeb; --color-components-checkbox-bg-disabled: #f2f4f7; --color-components-checkbox-border: #d0d5dc; --color-components-checkbox-border-hover: #98a2b2; - --color-components-checkbox-border-disabled: #18181b0a; + --color-components-checkbox-border-disabled: rgb(24 24 27 / 0.04); --color-components-checkbox-bg-unchecked: #ffffff; --color-components-checkbox-bg-unchecked-hover: #ffffff; --color-components-checkbox-bg-disabled-checked: #b2caff; @@ -104,15 +104,15 @@ html[data-theme="light"] { --color-components-radio-border-checked: #155aef; --color-components-radio-border-checked-hover: #004aeb; --color-components-radio-border-checked-disabled: #b2caff; - --color-components-radio-bg-disabled: #ffffff00; + --color-components-radio-bg-disabled: rgb(255 255 255 / 0); --color-components-radio-border: #d0d5dc; --color-components-radio-border-hover: #98a2b2; - --color-components-radio-border-disabled: #18181b0a; - --color-components-radio-bg: #ffffff00; - --color-components-radio-bg-hover: #ffffff00; + --color-components-radio-border-disabled: rgb(24 24 27 / 0.04); + --color-components-radio-bg: rgb(255 255 255 / 0); + --color-components-radio-bg-hover: rgb(255 255 255 / 0); --color-components-toggle-knob: #ffffff; - --color-components-toggle-knob-disabled: #fffffff2; + --color-components-toggle-knob-disabled: rgb(255 255 255 / 0.95); --color-components-toggle-bg: #155aef; --color-components-toggle-bg-hover: #004aeb; --color-components-toggle-bg-disabled: #d1e0ff; @@ -124,48 +124,52 @@ html[data-theme="light"] { --color-components-card-bg: #fcfcfd; --color-components-card-border: #ffffff; --color-components-card-bg-alt: #ffffff; + --color-components-card-bg-transparent: rgb(252 252 253 / 0); + --color-components-card-bg-alt-transparent: rgb(255 255 255 / 0); --color-components-menu-item-text: #495464; --color-components-menu-item-text-active: #18222f; --color-components-menu-item-text-hover: #354052; --color-components-menu-item-text-active-accent: #18222f; + --color-components-menu-item-bg-active: rgb(21 90 239 / 0.08); + --color-components-menu-item-bg-hover: rgb(200 206 218 / 0.2); --color-components-panel-bg: #ffffff; - --color-components-panel-bg-blur: #fffffff2; - --color-components-panel-border: #10182814; - --color-components-panel-border-subtle: #10182814; + --color-components-panel-bg-blur: rgb(255 255 255 / 0.95); + --color-components-panel-border: rgb(16 24 40 / 0.08); + --color-components-panel-border-subtle: rgb(16 24 40 / 0.08); --color-components-panel-gradient-2: #f9fafb; --color-components-panel-gradient-1: #ffffff; --color-components-panel-bg-alt: #f9fafb; --color-components-panel-on-panel-item-bg: #ffffff; --color-components-panel-on-panel-item-bg-hover: #f9fafb; --color-components-panel-on-panel-item-bg-alt: #f9fafb; - --color-components-panel-on-panel-item-bg-transparent: #fffffff2; - --color-components-panel-on-panel-item-bg-hover-transparent: #f9fafb00; - --color-components-panel-on-panel-item-bg-destructive-hover-transparent: #fef3f200; + --color-components-panel-on-panel-item-bg-transparent: rgb(255 255 255 / 0.95); + --color-components-panel-on-panel-item-bg-hover-transparent: rgb(249 250 251 / 0); + --color-components-panel-on-panel-item-bg-destructive-hover-transparent: rgb(254 243 242 / 0); - --color-components-panel-bg-transparent: #ffffff00; + --color-components-panel-bg-transparent: rgb(255 255 255 / 0); --color-components-main-nav-nav-button-text: #495464; --color-components-main-nav-nav-button-text-active: #155aef; - --color-components-main-nav-nav-button-bg: #ffffff00; + --color-components-main-nav-nav-button-bg: rgb(255 255 255 / 0); --color-components-main-nav-nav-button-bg-active: #fcfcfd; - --color-components-main-nav-nav-button-border: #fffffff2; - --color-components-main-nav-nav-button-bg-hover: #1018280a; + --color-components-main-nav-nav-button-border: rgb(255 255 255 / 0.95); + --color-components-main-nav-nav-button-bg-hover: rgb(16 24 40 / 0.04); --color-components-main-nav-nav-user-border: #ffffff; --color-components-slider-knob: #ffffff; --color-components-slider-knob-hover: #ffffff; - --color-components-slider-knob-disabled: #fffffff2; + --color-components-slider-knob-disabled: rgb(255 255 255 / 0.95); --color-components-slider-range: #296dff; --color-components-slider-track: #e9ebf0; - --color-components-slider-knob-border-hover: #10182833; - --color-components-slider-knob-border: #10182824; + --color-components-slider-knob-border-hover: rgb(16 24 40 / 0.2); + --color-components-slider-knob-border: rgb(16 24 40 / 0.14); --color-components-segmented-control-item-active-bg: #ffffff; --color-components-segmented-control-item-active-border: #ffffff; - --color-components-segmented-control-bg-normal: #c8ceda33; + --color-components-segmented-control-bg-normal: rgb(200 206 218 / 0.2); --color-components-segmented-control-item-active-accent-bg: #ffffff; --color-components-segmented-control-item-active-accent-border: #ffffff; @@ -181,94 +185,94 @@ html[data-theme="light"] { --color-components-badge-white-to-dark: #ffffff; --color-components-badge-status-light-success-bg: #47cd89; --color-components-badge-status-light-success-border-inner: #17b26a; - --color-components-badge-status-light-success-halo: #17b26a40; + --color-components-badge-status-light-success-halo: rgb(23 178 106 / 0.25); --color-components-badge-status-light-border-outer: #ffffff; - --color-components-badge-status-light-high-light: #ffffff4d; + --color-components-badge-status-light-high-light: rgb(255 255 255 / 0.3); --color-components-badge-status-light-warning-bg: #fdb022; --color-components-badge-status-light-warning-border-inner: #f79009; - --color-components-badge-status-light-warning-halo: #f7900940; + --color-components-badge-status-light-warning-halo: rgb(247 144 9 / 0.25); --color-components-badge-status-light-error-bg: #f97066; --color-components-badge-status-light-error-border-inner: #f04438; - --color-components-badge-status-light-error-halo: #f0443840; + --color-components-badge-status-light-error-halo: rgb(240 68 56 / 0.25); --color-components-badge-status-light-normal-bg: #36bffa; --color-components-badge-status-light-normal-border-inner: #0ba5ec; - --color-components-badge-status-light-normal-halo: #0ba5ec40; + --color-components-badge-status-light-normal-halo: rgb(11 165 236 / 0.25); --color-components-badge-status-light-disabled-bg: #98a2b2; --color-components-badge-status-light-disabled-border-inner: #676f83; - --color-components-badge-status-light-disabled-halo: #1018280a; + --color-components-badge-status-light-disabled-halo: rgb(16 24 40 / 0.04); - --color-components-badge-bg-green-soft: #17b26a14; - --color-components-badge-bg-orange-soft: #f7900914; - --color-components-badge-bg-red-soft: #f0443814; - --color-components-badge-bg-blue-light-soft: #0ba5ec14; - --color-components-badge-bg-gray-soft: #1018280a; - --color-components-badge-bg-dimm: #ffffff0d; + --color-components-badge-bg-green-soft: rgb(23 178 106 / 0.08); + --color-components-badge-bg-orange-soft: rgb(247 144 9 / 0.08); + --color-components-badge-bg-red-soft: rgb(240 68 56 / 0.08); + --color-components-badge-bg-blue-light-soft: rgb(11 165 236 / 0.08); + --color-components-badge-bg-gray-soft: rgb(16 24 40 / 0.04); + --color-components-badge-bg-dimm: rgb(255 255 255 / 0.05); --color-components-chart-line: #296dff; - --color-components-chart-area-1: #155aef24; - --color-components-chart-area-2: #155aef0a; + --color-components-chart-area-1: rgb(21 90 239 / 0.14); + --color-components-chart-area-2: rgb(21 90 239 / 0.04); --color-components-chart-current-1: #155aef; --color-components-chart-current-2: #d1e0ff; --color-components-chart-bg: #ffffff; - --color-components-actionbar-bg: #fffffff2; - --color-components-actionbar-border: #1018280a; + --color-components-actionbar-bg: rgb(255 255 255 / 0.95); + --color-components-actionbar-border: rgb(16 24 40 / 0.04); --color-components-actionbar-bg-accent: #f5f7ff; --color-components-actionbar-border-accent: #b2caff; --color-components-dropzone-bg-alt: #f2f4f7; --color-components-dropzone-bg: #f9fafb; - --color-components-dropzone-bg-accent: #155aef24; - --color-components-dropzone-border: #10182814; - --color-components-dropzone-border-alt: #10182833; + --color-components-dropzone-bg-accent: rgb(21 90 239 / 0.14); + --color-components-dropzone-border: rgb(16 24 40 / 0.08); + --color-components-dropzone-border-alt: rgb(16 24 40 / 0.2); --color-components-dropzone-border-accent: #84abff; --color-components-progress-brand-progress: #296dff; --color-components-progress-brand-border: #296dff; - --color-components-progress-brand-bg: #155aef0a; + --color-components-progress-brand-bg: rgb(21 90 239 / 0.04); --color-components-progress-white-progress: #ffffff; - --color-components-progress-white-border: #fffffff2; - --color-components-progress-white-bg: #ffffff03; + --color-components-progress-white-border: rgb(255 255 255 / 0.95); + --color-components-progress-white-bg: rgb(255 255 255 / 0.01); --color-components-progress-gray-progress: #98a2b2; --color-components-progress-gray-border: #98a2b2; - --color-components-progress-gray-bg: #c8ceda05; + --color-components-progress-gray-bg: rgb(200 206 218 / 0.02); --color-components-progress-warning-progress: #f79009; --color-components-progress-warning-border: #f79009; - --color-components-progress-warning-bg: #f790090a; + --color-components-progress-warning-bg: rgb(247 144 9 / 0.04); --color-components-progress-error-progress: #f04438; --color-components-progress-error-border: #f04438; - --color-components-progress-error-bg: #f044380a; + --color-components-progress-error-bg: rgb(240 68 56 / 0.04); --color-components-chat-input-audio-bg: #eff4ff; - --color-components-chat-input-audio-wave-default: #155aef33; - --color-components-chat-input-bg-mask-1: #ffffff03; + --color-components-chat-input-audio-wave-default: rgb(21 90 239 / 0.2); + --color-components-chat-input-bg-mask-1: rgb(255 255 255 / 0.01); --color-components-chat-input-bg-mask-2: #f2f4f7; --color-components-chat-input-border: #ffffff; --color-components-chat-input-audio-wave-active: #296dff; --color-components-chat-input-audio-bg-alt: #fcfcfd; --color-components-avatar-shape-fill-stop-0: #ffffff; - --color-components-avatar-shape-fill-stop-100: #ffffffe6; + --color-components-avatar-shape-fill-stop-100: rgb(255 255 255 / 0.9); - --color-components-avatar-bg-mask-stop-0: #ffffff1f; - --color-components-avatar-bg-mask-stop-100: #ffffff14; + --color-components-avatar-bg-mask-stop-0: rgb(255 255 255 / 0.12); + --color-components-avatar-bg-mask-stop-100: rgb(255 255 255 / 0.08); --color-components-avatar-default-avatar-bg: #d0d5dc; - --color-components-avatar-mask-darkmode-dimmed: #ffffff00; + --color-components-avatar-mask-darkmode-dimmed: rgb(255 255 255 / 0); --color-components-label-gray: #f2f4f7; --color-components-premium-badge-blue-bg-stop-0: #5289ff; --color-components-premium-badge-blue-bg-stop-100: #155aef; - --color-components-premium-badge-blue-stroke-stop-0: #fffffff2; + --color-components-premium-badge-blue-stroke-stop-0: rgb(255 255 255 / 0.95); --color-components-premium-badge-blue-stroke-stop-100: #155aef; --color-components-premium-badge-blue-text-stop-0: #f5f7ff; --color-components-premium-badge-blue-text-stop-100: #d1e0ff; @@ -276,14 +280,14 @@ html[data-theme="light"] { --color-components-premium-badge-blue-bg-stop-0-hover: #296dff; --color-components-premium-badge-blue-bg-stop-100-hover: #004aeb; --color-components-premium-badge-blue-glow-hover: #84abff; - --color-components-premium-badge-blue-stroke-stop-0-hover: #fffffff2; + --color-components-premium-badge-blue-stroke-stop-0-hover: rgb(255 255 255 / 0.95); --color-components-premium-badge-blue-stroke-stop-100-hover: #00329e; - --color-components-premium-badge-highlight-stop-0: #ffffff1f; - --color-components-premium-badge-highlight-stop-100: #ffffff4d; + --color-components-premium-badge-highlight-stop-0: rgb(255 255 255 / 0.12); + --color-components-premium-badge-highlight-stop-100: rgb(255 255 255 / 0.3); --color-components-premium-badge-indigo-bg-stop-0: #8098f9; --color-components-premium-badge-indigo-bg-stop-100: #444ce7; - --color-components-premium-badge-indigo-stroke-stop-0: #fffffff2; + --color-components-premium-badge-indigo-stroke-stop-0: rgb(255 255 255 / 0.95); --color-components-premium-badge-indigo-stroke-stop-100: #6172f3; --color-components-premium-badge-indigo-text-stop-0: #f5f8ff; --color-components-premium-badge-indigo-text-stop-100: #e0eaff; @@ -291,12 +295,12 @@ html[data-theme="light"] { --color-components-premium-badge-indigo-glow-hover: #a4bcfd; --color-components-premium-badge-indigo-bg-stop-0-hover: #6172f3; --color-components-premium-badge-indigo-bg-stop-100-hover: #2d31a6; - --color-components-premium-badge-indigo-stroke-stop-0-hover: #fffffff2; + --color-components-premium-badge-indigo-stroke-stop-0-hover: rgb(255 255 255 / 0.95); --color-components-premium-badge-indigo-stroke-stop-100-hover: #2d31a6; --color-components-premium-badge-grey-bg-stop-0: #98a2b2; --color-components-premium-badge-grey-bg-stop-100: #676f83; - --color-components-premium-badge-grey-stroke-stop-0: #fffffff2; + --color-components-premium-badge-grey-stroke-stop-0: rgb(255 255 255 / 0.95); --color-components-premium-badge-grey-stroke-stop-100: #676f83; --color-components-premium-badge-grey-text-stop-0: #fcfcfd; --color-components-premium-badge-grey-text-stop-100: #f2f4f7; @@ -304,12 +308,12 @@ html[data-theme="light"] { --color-components-premium-badge-grey-glow-hover: #d0d5dc; --color-components-premium-badge-grey-bg-stop-0-hover: #676f83; --color-components-premium-badge-grey-bg-stop-100-hover: #354052; - --color-components-premium-badge-grey-stroke-stop-0-hover: #fffffff2; + --color-components-premium-badge-grey-stroke-stop-0-hover: rgb(255 255 255 / 0.95); --color-components-premium-badge-grey-stroke-stop-100-hover: #354052; --color-components-premium-badge-orange-bg-stop-0: #ff692e; --color-components-premium-badge-orange-bg-stop-100: #e04f16; - --color-components-premium-badge-orange-stroke-stop-0: #fffffff2; + --color-components-premium-badge-orange-stroke-stop-0: rgb(255 255 255 / 0.95); --color-components-premium-badge-orange-stroke-stop-100: #e62e05; --color-components-premium-badge-orange-text-stop-0: #fefaf5; --color-components-premium-badge-orange-text-stop-100: #fdead7; @@ -317,14 +321,14 @@ html[data-theme="light"] { --color-components-premium-badge-orange-glow-hover: #f7b27a; --color-components-premium-badge-orange-bg-stop-0-hover: #ff4405; --color-components-premium-badge-orange-bg-stop-100-hover: #b93815; - --color-components-premium-badge-orange-stroke-stop-0-hover: #fffffff2; + --color-components-premium-badge-orange-stroke-stop-0-hover: rgb(255 255 255 / 0.95); --color-components-premium-badge-orange-stroke-stop-100-hover: #bc1b06; - --color-components-progress-bar-bg: #155aef0a; - --color-components-progress-bar-progress: #155aef24; - --color-components-progress-bar-border: #1018280a; + --color-components-progress-bar-bg: rgb(21 90 239 / 0.04); + --color-components-progress-bar-progress: rgb(21 90 239 / 0.14); + --color-components-progress-bar-border: rgb(16 24 40 / 0.04); --color-components-progress-bar-progress-solid: #296dff; - --color-components-progress-bar-progress-highlight: #155aef33; + --color-components-progress-bar-progress-highlight: rgb(21 90 239 / 0.2); --color-components-icon-bg-red-solid: #d92d20; --color-components-icon-bg-rose-solid: #e31b54; @@ -356,7 +360,7 @@ html[data-theme="light"] { --color-text-primary: #101828; --color-text-secondary: #354052; --color-text-tertiary: #676f83; - --color-text-quaternary: #1018284d; + --color-text-quaternary: rgb(16 24 40 / 0.3); --color-text-destructive: #d92d20; --color-text-success: #079455; --color-text-warning: #dc6803; @@ -369,75 +373,80 @@ html[data-theme="light"] { --color-text-disabled: #d0d5dc; --color-text-accent-secondary: #296dff; --color-text-accent-light-mode-only: #155aef; - --color-text-text-selected: #155aef24; - --color-text-secondary-on-surface: #ffffffe6; + --color-text-text-selected: rgb(21 90 239 / 0.14); + --color-text-secondary-on-surface: rgb(255 255 255 / 0.9); --color-text-logo-text: #18222f; --color-text-empty-state-icon: #d0d5dc; --color-text-inverted: #000000; - --color-text-inverted-dimmed: #000000f2; + --color-text-inverted-dimmed: rgb(0 0 0 / 0.95); --color-background-body: #f2f4f7; --color-background-default-subtle: #fcfcfd; --color-background-neutral-subtle: #f9fafb; - --color-background-sidenav-bg: #ffffffcc; + --color-background-sidenav-bg: rgb(255 255 255 / 0.8); --color-background-default: #ffffff; --color-background-soft: #f9fafb; --color-background-gradient-bg-fill-chat-bg-1: #f9fafb; --color-background-gradient-bg-fill-chat-bg-2: #f2f4f7; --color-background-gradient-bg-fill-chat-bubble-bg-1: #ffffff; - --color-background-gradient-bg-fill-chat-bubble-bg-2: #ffffff99; - --color-background-gradient-bg-fill-chat-bubble-bg-3: #e1effe; - --color-background-gradient-bg-fill-debug-bg-1: #ffffff00; - --color-background-gradient-bg-fill-debug-bg-2: #c8ceda24; - - --color-background-gradient-mask-gray: #c8ceda33; - --color-background-gradient-mask-transparent: #ffffff00; - --color-background-gradient-mask-input-clear-2: #e9ebf000; + --color-background-gradient-bg-fill-chat-bubble-bg-2: rgb(255 255 255 / 0.6); + --color-background-gradient-bg-fill-debug-bg-1: rgb(255 255 255 / 0); + --color-background-gradient-bg-fill-debug-bg-2: rgb(200 206 218 / 0.14); + + --color-background-gradient-mask-gray: rgb(200 206 218 / 0.2); + --color-background-gradient-mask-transparent: rgb(255 255 255 / 0); + --color-background-gradient-mask-input-clear-2: rgb(233 235 240 / 0); --color-background-gradient-mask-input-clear-1: #e9ebf0; - --color-background-gradient-mask-transparent-dark: #00000000; - --color-background-gradient-mask-side-panel-2: #1018284d; - --color-background-gradient-mask-side-panel-1: #10182805; + --color-background-gradient-mask-transparent-dark: rgb(0 0 0 / 0); + --color-background-gradient-mask-side-panel-2: rgb(16 24 40 / 0.3); + --color-background-gradient-mask-side-panel-1: rgb(16 24 40 / 0.02); --color-background-default-burn: #e9ebf0; - --color-background-overlay-fullscreen: #f9fafbf2; - --color-background-default-lighter: #ffffff80; + --color-background-overlay-fullscreen: rgb(249 250 251 / 0.95); + --color-background-default-lighter: rgb(255 255 255 / 0.5); --color-background-section: #f9fafb; - --color-background-interaction-from-bg-1: #c8ceda33; - --color-background-interaction-from-bg-2: #c8ceda24; + --color-background-interaction-from-bg-1: rgb(200 206 218 / 0.2); + --color-background-interaction-from-bg-2: rgb(200 206 218 / 0.14); --color-background-section-burn: #f2f4f7; --color-background-default-dodge: #ffffff; - --color-background-overlay: #10182899; + --color-background-overlay: rgb(16 24 40 / 0.6); --color-background-default-dimmed: #e9ebf0; --color-background-default-hover: #f9fafb; - --color-background-overlay-alt: #10182866; - --color-background-surface-white: #fffffff2; - --color-background-overlay-destructive: #f044384d; - --color-background-overlay-backdrop: #f2f4f7f2; - - --color-shadow-shadow-1: #09090b08; - --color-shadow-shadow-3: #09090b0d; - --color-shadow-shadow-4: #09090b0f; - --color-shadow-shadow-5: #09090b14; - --color-shadow-shadow-6: #09090b1a; - --color-shadow-shadow-7: #09090b1f; - --color-shadow-shadow-8: #09090b24; - --color-shadow-shadow-9: #09090b2e; - --color-shadow-shadow-2: #09090b0a; - --color-shadow-shadow-10: #09090b0d; + --color-background-overlay-alt: rgb(16 24 40 / 0.4); + --color-background-surface-white: rgb(255 255 255 / 0.95); + --color-background-overlay-destructive: rgb(240 68 56 / 0.3); + --color-background-overlay-backdrop: rgb(242 244 247 / 0.95); + --color-background-body-transparent: rgb(242 244 247 / 0); + + --color-shadow-shadow-1: rgb(9 9 11 / 0.03); + --color-shadow-shadow-3: rgb(9 9 11 / 0.05); + --color-shadow-shadow-4: rgb(9 9 11 / 0.06); + --color-shadow-shadow-5: rgb(9 9 11 / 0.08); + --color-shadow-shadow-6: rgb(9 9 11 / 0.1); + --color-shadow-shadow-7: rgb(9 9 11 / 0.12); + --color-shadow-shadow-8: rgb(9 9 11 / 0.14); + --color-shadow-shadow-9: rgb(9 9 11 / 0.18); + --color-shadow-shadow-2: rgb(9 9 11 / 0.04); + --color-shadow-shadow-10: rgb(9 9 11 / 0.05); --color-workflow-block-border: #ffffff; --color-workflow-block-parma-bg: #f2f4f7; --color-workflow-block-bg: #fcfcfd; - --color-workflow-block-bg-transparent: #fcfcfde6; - --color-workflow-block-border-highlight: #155aef24; + --color-workflow-block-bg-transparent: rgb(252 252 253 / 0.9); + --color-workflow-block-border-highlight: rgb(21 90 239 / 0.14); + --color-workflow-block-wrapper-bg-1: #e9ebf0; + --color-workflow-block-wrapper-bg-2: rgb(233 235 240 / 0.2); - --color-workflow-canvas-workflow-dot-color: #8585ad26; + --color-workflow-canvas-workflow-dot-color: rgb(133 133 173 / 0.15); --color-workflow-canvas-workflow-bg: #f2f4f7; + --color-workflow-canvas-workflow-top-bar-1: #f2f4f7; + --color-workflow-canvas-workflow-top-bar-2: rgb(242 244 247 / 0.24); + --color-workflow-canvas-canvas-overlay: rgb(242 244 247 / 0.8); --color-workflow-link-line-active: #296dff; --color-workflow-link-line-normal: #d0d5dc; --color-workflow-link-line-handle: #296dff; - --color-workflow-link-line-normal-transparent: #d0d5dc33; + --color-workflow-link-line-normal-transparent: rgb(208 213 220 / 0.2); --color-workflow-link-line-failure-active: #f79009; --color-workflow-link-line-failure-handle: #f79009; --color-workflow-link-line-failure-button-bg: #dc6803; @@ -450,73 +459,74 @@ html[data-theme="light"] { --color-workflow-link-line-error-handle: #f04438; --color-workflow-minimap-bg: #e9ebf0; - --color-workflow-minimap-block: #c8ceda4d; + --color-workflow-minimap-block: rgb(200 206 218 / 0.3); --color-workflow-display-success-bg: #ecfdf3; - --color-workflow-display-success-border-1: #17b26acc; - --color-workflow-display-success-border-2: #17b26a80; - --color-workflow-display-success-vignette-color: #17b26a33; - --color-workflow-display-success-bg-line-pattern: #17b26a4d; - - --color-workflow-display-glass-1: #ffffff1f; - --color-workflow-display-glass-2: #ffffff80; - --color-workflow-display-vignette-dark: #0000001f; - --color-workflow-display-highlight: #ffffff80; - --color-workflow-display-outline: #0000000d; + --color-workflow-display-success-border-1: rgb(23 178 106 / 0.8); + --color-workflow-display-success-border-2: rgb(23 178 106 / 0.5); + --color-workflow-display-success-vignette-color: rgb(23 178 106 / 0.2); + --color-workflow-display-success-bg-line-pattern: rgb(23 178 106 / 0.3); + + --color-workflow-display-glass-1: rgb(255 255 255 / 0.12); + --color-workflow-display-glass-2: rgb(255 255 255 / 0.5); + --color-workflow-display-vignette-dark: rgb(0 0 0 / 0.12); + --color-workflow-display-highlight: rgb(255 255 255 / 0.5); + --color-workflow-display-outline: rgb(0 0 0 / 0.05); --color-workflow-display-error-bg: #fef3f2; - --color-workflow-display-error-bg-line-pattern: #f044384d; - --color-workflow-display-error-border-1: #f04438cc; - --color-workflow-display-error-border-2: #f0443880; - --color-workflow-display-error-vignette-color: #f0443833; + --color-workflow-display-error-bg-line-pattern: rgb(240 68 56 / 0.3); + --color-workflow-display-error-border-1: rgb(240 68 56 / 0.8); + --color-workflow-display-error-border-2: rgb(240 68 56 / 0.5); + --color-workflow-display-error-vignette-color: rgb(240 68 56 / 0.2); --color-workflow-display-warning-bg: #fffaeb; - --color-workflow-display-warning-bg-line-pattern: #f790094d; - --color-workflow-display-warning-border-1: #f79009cc; - --color-workflow-display-warning-border-2: #f7900980; - --color-workflow-display-warning-vignette-color: #f7900933; + --color-workflow-display-warning-bg-line-pattern: rgb(247 144 9 / 0.3); + --color-workflow-display-warning-border-1: rgb(247 144 9 / 0.8); + --color-workflow-display-warning-border-2: rgb(247 144 9 / 0.5); + --color-workflow-display-warning-vignette-color: rgb(247 144 9 / 0.2); --color-workflow-display-normal-bg: #f0f9ff; - --color-workflow-display-normal-bg-line-pattern: #0ba5ec4d; - --color-workflow-display-normal-border-1: #0ba5eccc; - --color-workflow-display-normal-border-2: #0ba5ec80; - --color-workflow-display-normal-vignette-color: #0ba5ec33; + --color-workflow-display-normal-bg-line-pattern: rgb(11 165 236 / 0.3); + --color-workflow-display-normal-border-1: rgb(11 165 236 / 0.8); + --color-workflow-display-normal-border-2: rgb(11 165 236 / 0.5); + --color-workflow-display-normal-vignette-color: rgb(11 165 236 / 0.2); --color-workflow-display-disabled-bg: #f9fafb; - --color-workflow-display-disabled-bg-line-pattern: #c8ceda4d; - --color-workflow-display-disabled-border-1: #c8ceda99; - --color-workflow-display-disabled-border-2: #c8ceda66; - --color-workflow-display-disabled-vignette-color: #c8ceda66; - --color-workflow-display-disabled-outline: #00000000; - - --color-workflow-workflow-progress-bg-1: #c8ceda33; - --color-workflow-workflow-progress-bg-2: #c8ceda0a; - - --color-divider-subtle: #1018280a; - --color-divider-regular: #10182814; - --color-divider-deep: #10182824; - --color-divider-burn: #1018280a; - --color-divider-intense: #1018284d; + --color-workflow-display-disabled-bg-line-pattern: rgb(200 206 218 / 0.3); + --color-workflow-display-disabled-border-1: rgb(200 206 218 / 0.6); + --color-workflow-display-disabled-border-2: rgb(200 206 218 / 0.4); + --color-workflow-display-disabled-vignette-color: rgb(200 206 218 / 0.4); + --color-workflow-display-disabled-outline: rgb(0 0 0 / 0); + + --color-workflow-workflow-progress-bg-1: rgb(200 206 218 / 0.2); + --color-workflow-workflow-progress-bg-2: rgb(200 206 218 / 0.04); + + --color-divider-subtle: rgb(16 24 40 / 0.04); + --color-divider-regular: rgb(16 24 40 / 0.08); + --color-divider-deep: rgb(16 24 40 / 0.14); + --color-divider-burn: rgb(16 24 40 / 0.04); + --color-divider-intense: rgb(16 24 40 / 0.3); --color-divider-solid: #d0d5dc; --color-divider-solid-alt: #98a2b2; - --color-state-base-hover: #c8ceda33; - --color-state-base-active: #c8ceda66; - --color-state-base-hover-alt: #c8ceda66; - --color-state-base-handle: #10182833; - --color-state-base-handle-hover: #1018284d; - --color-state-base-hover-subtle: #c8ceda14; + --color-state-base-hover: rgb(200 206 218 / 0.2); + --color-state-base-active: rgb(200 206 218 / 0.4); + --color-state-base-hover-alt: rgb(200 206 218 / 0.4); + --color-state-base-handle: rgb(16 24 40 / 0.2); + --color-state-base-handle-hover: rgb(16 24 40 / 0.3); + --color-state-base-hover-subtle: rgb(200 206 218 / 0.08); --color-state-accent-hover: #eff4ff; - --color-state-accent-active: #155aef14; + --color-state-accent-active: rgb(21 90 239 / 0.08); --color-state-accent-hover-alt: #d1e0ff; --color-state-accent-solid: #296dff; - --color-state-accent-active-alt: #155aef24; + --color-state-accent-active-alt: rgb(21 90 239 / 0.14); --color-state-destructive-hover: #fef3f2; --color-state-destructive-hover-alt: #fee4e2; --color-state-destructive-active: #fecdca; --color-state-destructive-solid: #f04438; --color-state-destructive-border: #fda29b; + --color-state-destructive-hover-transparent: rgb(254 243 242 / 0); --color-state-success-hover: #ecfdf3; --color-state-success-hover-alt: #dcfae6; @@ -527,10 +537,12 @@ html[data-theme="light"] { --color-state-warning-hover-alt: #fef0c7; --color-state-warning-active: #fedf89; --color-state-warning-solid: #f79009; + --color-state-warning-hover-transparent: rgb(255 250 235 / 0); --color-effects-highlight: #ffffff; - --color-effects-highlight-lightmode-off: #ffffff00; + --color-effects-highlight-lightmode-off: rgb(255 255 255 / 0); --color-effects-image-frame: #ffffff; + --color-effects-icon-border: rgb(16 24 40 / 0.08); --color-util-colors-orange-dark-orange-dark-50: #fff4ed; --color-util-colors-orange-dark-orange-dark-100: #ffe6d5; @@ -549,7 +561,7 @@ html[data-theme="light"] { --color-util-colors-orange-orange-500: #ef6820; --color-util-colors-orange-orange-600: #e04f16; --color-util-colors-orange-orange-700: #b93815; - --color-util-colors-orange-orange-100-transparent: #fdead700; + --color-util-colors-orange-orange-100-transparent: rgb(253 234 215 / 0); --color-util-colors-pink-pink-50: #fdf2fa; --color-util-colors-pink-pink-100: #fce7f6; @@ -735,6 +747,9 @@ html[data-theme="light"] { --color-third-party-aws-alt: #0f1824; --color-saas-background: #fcfcfd; - --color-saas-pricing-grid-bg: #c8ceda80; + --color-saas-pricing-grid-bg: rgb(200 206 218 / 0.5); + + --color-dify-logo-dify-logo-blue: #0033ff; + --color-dify-logo-dify-logo-black: #000000; } diff --git a/web/themes/manual-dark.css b/web/themes/manual-dark.css index ed5f12bbc4..316fd7573a 100644 --- a/web/themes/manual-dark.css +++ b/web/themes/manual-dark.css @@ -63,4 +63,5 @@ html[data-theme="dark"] { --color-premium-badge-border-highlight-color: #ffffff33; --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); --color-grid-mask-background: linear-gradient(0deg, rgba(0, 0, 0, 0.00) 0%, rgba(24, 24, 25, 0.1) 62.25%, rgba(24, 24, 25, 0.10) 100%); + --color-background-gradient-bg-fill-chat-bubble-bg-3: #27314d; } diff --git a/web/themes/manual-light.css b/web/themes/manual-light.css index c20155036e..1198ab8396 100644 --- a/web/themes/manual-light.css +++ b/web/themes/manual-light.css @@ -63,4 +63,5 @@ html[data-theme="light"] { --color-premium-badge-border-highlight-color: #fffffff2; --color-price-enterprise-background: linear-gradient(180deg, rgba(185, 211, 234, 0.00) 0%, rgba(180, 209, 234, 0.92) 100%); --color-grid-mask-background: linear-gradient(0deg, #FFF 0%, rgba(217, 217, 217, 0.10) 62.25%, rgba(217, 217, 217, 0.10) 100%); + --color-background-gradient-bg-fill-chat-bubble-bg-3: #e1effe; } diff --git a/web/themes/tailwind-theme-var-define.ts b/web/themes/tailwind-theme-var-define.ts index 11189441ee..66a34b06ca 100644 --- a/web/themes/tailwind-theme-var-define.ts +++ b/web/themes/tailwind-theme-var-define.ts @@ -124,11 +124,15 @@ const vars = { 'components-card-bg': 'var(--color-components-card-bg)', 'components-card-border': 'var(--color-components-card-border)', 'components-card-bg-alt': 'var(--color-components-card-bg-alt)', + 'components-card-bg-transparent': 'var(--color-components-card-bg-transparent)', + 'components-card-bg-alt-transparent': 'var(--color-components-card-bg-alt-transparent)', 'components-menu-item-text': 'var(--color-components-menu-item-text)', 'components-menu-item-text-active': 'var(--color-components-menu-item-text-active)', 'components-menu-item-text-hover': 'var(--color-components-menu-item-text-hover)', 'components-menu-item-text-active-accent': 'var(--color-components-menu-item-text-active-accent)', + 'components-menu-item-bg-active': 'var(--color-components-menu-item-bg-active)', + 'components-menu-item-bg-hover': 'var(--color-components-menu-item-bg-hover)', 'components-panel-bg': 'var(--color-components-panel-bg)', 'components-panel-bg-blur': 'var(--color-components-panel-bg-blur)', @@ -386,7 +390,6 @@ const vars = { 'background-gradient-bg-fill-chat-bg-2': 'var(--color-background-gradient-bg-fill-chat-bg-2)', 'background-gradient-bg-fill-chat-bubble-bg-1': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-1)', 'background-gradient-bg-fill-chat-bubble-bg-2': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-2)', - 'background-gradient-bg-fill-chat-bubble-bg-3': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-3)', 'background-gradient-bg-fill-debug-bg-1': 'var(--color-background-gradient-bg-fill-debug-bg-1)', 'background-gradient-bg-fill-debug-bg-2': 'var(--color-background-gradient-bg-fill-debug-bg-2)', @@ -413,6 +416,7 @@ const vars = { 'background-surface-white': 'var(--color-background-surface-white)', 'background-overlay-destructive': 'var(--color-background-overlay-destructive)', 'background-overlay-backdrop': 'var(--color-background-overlay-backdrop)', + 'background-body-transparent': 'var(--color-background-body-transparent)', 'shadow-shadow-1': 'var(--color-shadow-shadow-1)', 'shadow-shadow-3': 'var(--color-shadow-shadow-3)', @@ -430,9 +434,14 @@ const vars = { 'workflow-block-bg': 'var(--color-workflow-block-bg)', 'workflow-block-bg-transparent': 'var(--color-workflow-block-bg-transparent)', 'workflow-block-border-highlight': 'var(--color-workflow-block-border-highlight)', + 'workflow-block-wrapper-bg-1': 'var(--color-workflow-block-wrapper-bg-1)', + 'workflow-block-wrapper-bg-2': 'var(--color-workflow-block-wrapper-bg-2)', 'workflow-canvas-workflow-dot-color': 'var(--color-workflow-canvas-workflow-dot-color)', 'workflow-canvas-workflow-bg': 'var(--color-workflow-canvas-workflow-bg)', + 'workflow-canvas-workflow-top-bar-1': 'var(--color-workflow-canvas-workflow-top-bar-1)', + 'workflow-canvas-workflow-top-bar-2': 'var(--color-workflow-canvas-workflow-top-bar-2)', + 'workflow-canvas-canvas-overlay': 'var(--color-workflow-canvas-canvas-overlay)', 'workflow-link-line-active': 'var(--color-workflow-link-line-active)', 'workflow-link-line-normal': 'var(--color-workflow-link-line-normal)', @@ -517,6 +526,7 @@ const vars = { 'state-destructive-active': 'var(--color-state-destructive-active)', 'state-destructive-solid': 'var(--color-state-destructive-solid)', 'state-destructive-border': 'var(--color-state-destructive-border)', + 'state-destructive-hover-transparent': 'var(--color-state-destructive-hover-transparent)', 'state-success-hover': 'var(--color-state-success-hover)', 'state-success-hover-alt': 'var(--color-state-success-hover-alt)', @@ -527,10 +537,12 @@ const vars = { 'state-warning-hover-alt': 'var(--color-state-warning-hover-alt)', 'state-warning-active': 'var(--color-state-warning-active)', 'state-warning-solid': 'var(--color-state-warning-solid)', + 'state-warning-hover-transparent': 'var(--color-state-warning-hover-transparent)', 'effects-highlight': 'var(--color-effects-highlight)', 'effects-highlight-lightmode-off': 'var(--color-effects-highlight-lightmode-off)', 'effects-image-frame': 'var(--color-effects-image-frame)', + 'effects-icon-border': 'var(--color-effects-icon-border)', 'util-colors-orange-dark-orange-dark-50': 'var(--color-util-colors-orange-dark-orange-dark-50)', 'util-colors-orange-dark-orange-dark-100': 'var(--color-util-colors-orange-dark-orange-dark-100)', @@ -737,5 +749,8 @@ const vars = { 'saas-background': 'var(--color-saas-background)', 'saas-pricing-grid-bg': 'var(--color-saas-pricing-grid-bg)', + 'dify-logo-dify-logo-blue': 'var(--color-dify-logo-dify-logo-blue)', + 'dify-logo-dify-logo-black': 'var(--color-dify-logo-dify-logo-black)', + } export default vars diff --git a/web/types/app.ts b/web/types/app.ts index e4227adbe9..3de5c446ec 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -90,6 +90,7 @@ export type TextTypeFormItem = { variable: string required: boolean max_length: number + hide: boolean } export type SelectTypeFormItem = { @@ -98,6 +99,7 @@ export type SelectTypeFormItem = { variable: string required: boolean options: string[] + hide: boolean } export type ParagraphTypeFormItem = { @@ -105,6 +107,7 @@ export type ParagraphTypeFormItem = { label: string variable: string required: boolean + hide: boolean } /** * User Input Form Item diff --git a/web/types/feature.ts b/web/types/feature.ts index cc945754b1..5787c2661f 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -13,12 +13,23 @@ export enum LicenseStatus { LOST = 'lost', } +export enum InstallationScope { + ALL = 'all', + NONE = 'none', + OFFICIAL_ONLY = 'official_only', + OFFICIAL_AND_PARTNER = 'official_and_specific_partners', +} + type License = { status: LicenseStatus expired_at: string | null } export type SystemFeatures = { + plugin_installation_permission: { + plugin_installation_scope: InstallationScope, + restrict_to_marketplace_only: boolean + }, sso_enforced_for_signin: boolean sso_enforced_for_signin_protocol: SSOProtocol | '' sso_enforced_for_web: boolean @@ -50,6 +61,10 @@ export type SystemFeatures = { } export const defaultSystemFeatures: SystemFeatures = { + plugin_installation_permission: { + plugin_installation_scope: InstallationScope.ALL, + restrict_to_marketplace_only: false, + }, sso_enforced_for_signin: false, sso_enforced_for_signin_protocol: '', sso_enforced_for_web: false, @@ -82,3 +97,27 @@ export const defaultSystemFeatures: SystemFeatures = { allow_email_password_login: false, }, } + +export enum DatasetAttr { + DATA_API_PREFIX = 'data-api-prefix', + DATA_PUBLIC_API_PREFIX = 'data-public-api-prefix', + DATA_MARKETPLACE_API_PREFIX = 'data-marketplace-api-prefix', + DATA_MARKETPLACE_URL_PREFIX = 'data-marketplace-url-prefix', + DATA_PUBLIC_EDITION = 'data-public-edition', + DATA_PUBLIC_SUPPORT_MAIL_LOGIN = 'data-public-support-mail-login', + DATA_PUBLIC_SENTRY_DSN = 'data-public-sentry-dsn', + DATA_PUBLIC_MAINTENANCE_NOTICE = 'data-public-maintenance-notice', + DATA_PUBLIC_SITE_ABOUT = 'data-public-site-about', + DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS = 'data-public-text-generation-timeout-ms', + DATA_PUBLIC_MAX_TOOLS_NUM = 'data-public-max-tools-num', + DATA_PUBLIC_MAX_PARALLEL_LIMIT = 'data-public-max-parallel-limit', + DATA_PUBLIC_TOP_K_MAX_VALUE = 'data-public-top-k-max-value', + DATA_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH = 'data-public-indexing-max-segmentation-tokens-length', + DATA_PUBLIC_LOOP_NODE_MAX_COUNT = 'data-public-loop-node-max-count', + DATA_PUBLIC_MAX_ITERATIONS_NUM = 'data-public-max-iterations-num', + DATA_PUBLIC_MAX_TREE_DEPTH = 'data-public-max-tree-depth', + DATA_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME = 'data-public-allow-unsafe-data-scheme', + DATA_PUBLIC_ENABLE_WEBSITE_JINAREADER = 'data-public-enable-website-jinareader', + DATA_PUBLIC_ENABLE_WEBSITE_FIRECRAWL = 'data-public-enable-website-firecrawl', + DATA_PUBLIC_ENABLE_WEBSITE_WATERCRAWL = 'data-public-enable-website-watercrawl', +} diff --git a/web/types/workflow.ts b/web/types/workflow.ts index bd7334a261..1cbcc942a5 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -1,7 +1,10 @@ import type { Viewport } from 'reactflow' -import type { BlockEnum, ConversationVariable, Edge, EnvironmentVariable, Node } from '@/app/components/workflow/types' +import type { BlockEnum, CommonNodeType, ConversationVariable, Edge, EnvironmentVariable, InputVar, Node, ValueSelector, VarType, Variable } from '@/app/components/workflow/types' import type { TransferMethod } from '@/types/app' import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' +import type { BeforeRunFormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form' +import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel' +import type { MutableRefObject } from 'react' export type AgentLogItem = { node_execution_id: string, @@ -35,7 +38,7 @@ export type NodeTracing = { title: string inputs: any process_data: any - outputs?: any + outputs?: Record<string, any> status: string parallel_run_id?: string error?: string @@ -152,7 +155,6 @@ export type WorkflowStartedResponse = { data: { id: string workflow_id: string - sequence_number: number created_at: number } } @@ -198,6 +200,7 @@ export type FileResponse = { type: string url: string upload_file_id: string + remote_url: string } export type NodeFinishedResponse = { @@ -289,7 +292,6 @@ export type AgentLogResponse = { export type WorkflowRunHistory = { id: string - sequence_number: number version: string conversation_id?: string message_id?: string @@ -352,3 +354,48 @@ export type UpdateWorkflowParams = { title: string releaseNotes: string } + +export type PanelExposedType = { + singleRunParams: Pick<BeforeRunFormProps, 'forms'> & Partial<SpecialResultPanelProps> +} + +export type PanelProps = { + getInputVars: (textList: string[]) => InputVar[] + toVarInputs: (variables: Variable[]) => InputVar[] + runInputData: Record<string, any> + runInputDataRef: MutableRefObject<Record<string, any>> + setRunInputData: (data: Record<string, any>) => void + runResult: any +} + +export type NodeRunResult = NodeTracing + +// Var Inspect +export enum VarInInspectType { + conversation = 'conversation', + environment = 'env', + node = 'node', + system = 'sys', +} + +export type VarInInspect = { + id: string + type: VarInInspectType + name: string + description: string + selector: ValueSelector // can get node id from selector[0] + value_type: VarType + value: any + edited: boolean + visible: boolean +} + +export type NodeWithVar = { + nodeId: string + nodePayload: CommonNodeType + nodeType: BlockEnum + title: string + vars: VarInInspect[] + isSingRunRunning?: boolean + isValueFetched?: boolean +} diff --git a/web/utils/completion-params.ts b/web/utils/completion-params.ts new file mode 100644 index 0000000000..b46c3ab720 --- /dev/null +++ b/web/utils/completion-params.ts @@ -0,0 +1,88 @@ +import type { FormValue, ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' + +export const mergeValidCompletionParams = ( + oldParams: FormValue | undefined, + rules: ModelParameterRule[], +): { params: FormValue; removedDetails: Record<string, string> } => { + if (!oldParams || Object.keys(oldParams).length === 0) + return { params: {}, removedDetails: {} } + + const acceptedKeys = new Set(rules.map(r => r.name)) + const ruleMap: Record<string, ModelParameterRule> = {} + rules.forEach((r) => { + ruleMap[r.name] = r + }) + + const nextParams: FormValue = {} + const removedDetails: Record<string, string> = {} + + Object.entries(oldParams).forEach(([key, value]) => { + if (!acceptedKeys.has(key)) { + removedDetails[key] = 'unsupported' + return + } + + const rule = ruleMap[key] + if (!rule) { + removedDetails[key] = 'unsupported' + return + } + + switch (rule.type) { + case 'int': + case 'float': { + if (typeof value !== 'number') { + removedDetails[key] = 'invalid type' + return + } + const min = rule.min ?? Number.NEGATIVE_INFINITY + const max = rule.max ?? Number.POSITIVE_INFINITY + if (value < min || value > max) { + removedDetails[key] = `out of range (${min}-${max})` + return + } + nextParams[key] = value + return + } + case 'boolean': { + if (typeof value !== 'boolean') { + removedDetails[key] = 'invalid type' + return + } + nextParams[key] = value + return + } + case 'string': + case 'text': { + if (typeof value !== 'string') { + removedDetails[key] = 'invalid type' + return + } + if (Array.isArray(rule.options) && rule.options.length) { + if (!(rule.options as string[]).includes(value)) { + removedDetails[key] = 'unsupported option' + return + } + } + nextParams[key] = value + return + } + default: { + removedDetails[key] = `unsupported rule type: ${(rule as any)?.type ?? 'unknown'}` + } + } + }) + + return { params: nextParams, removedDetails } +} + +export const fetchAndMergeValidCompletionParams = async ( + provider: string, + modelId: string, + oldParams: FormValue | undefined, +): Promise<{ params: FormValue; removedDetails: Record<string, string> }> => { + const { fetchModelParameterRules } = await import('@/service/common') + const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + return mergeValidCompletionParams(oldParams, parameterRules ?? []) +} diff --git a/web/utils/model-config.ts b/web/utils/model-config.ts index 74d8848a98..330d8f9b52 100644 --- a/web/utils/model-config.ts +++ b/web/utils/model-config.ts @@ -40,6 +40,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | max_length: content.max_length, options: [], is_context_var, + hide: content.hide, }) } else if (type === 'number') { @@ -49,6 +50,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | required: content.required, type, options: [], + hide: content.hide, }) } else if (type === 'select') { @@ -59,6 +61,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | type: 'select', options: content.options, is_context_var, + hide: content.hide, }) } else if (type === 'file') { @@ -73,6 +76,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | allowed_file_upload_methods: content.allowed_file_upload_methods, number_limits: 1, }, + hide: content.hide, }) } else if (type === 'file-list') { @@ -87,6 +91,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | allowed_file_upload_methods: content.allowed_file_upload_methods, number_limits: content.max_length, }, + hide: content.hide, }) } else { @@ -100,6 +105,7 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | icon: content.icon, icon_background: content.icon_background, is_context_var, + hide: content.hide, }) } }) @@ -119,6 +125,7 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[ required: item.required !== false, // default true max_length: item.max_length, default: '', + hide: item.hide, }, } as any) return @@ -130,6 +137,7 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[ variable: item.key, required: item.required !== false, // default true default: '', + hide: item.hide, }, } as any) } @@ -141,6 +149,7 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[ required: item.required !== false, // default true options: item.options, default: '', + hide: item.hide, }, } as any) } @@ -155,6 +164,7 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[ required: item.required, icon: item.icon, icon_background: item.icon_background, + hide: item.hide, }, } as any) } diff --git a/web/utils/plugin-version-feature.spec.ts b/web/utils/plugin-version-feature.spec.ts new file mode 100644 index 0000000000..12ca239aa9 --- /dev/null +++ b/web/utils/plugin-version-feature.spec.ts @@ -0,0 +1,26 @@ +import { isSupportMCP } from './plugin-version-feature' + +describe('plugin-version-feature', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('isSupportMCP', () => { + it('should call isEqualOrLaterThanVersion with the correct parameters', () => { + expect(isSupportMCP('0.0.3')).toBe(true) + expect(isSupportMCP('1.0.0')).toBe(true) + }) + + it('should return true when version is equal to the supported MCP version', () => { + const mockVersion = '0.0.2' + const result = isSupportMCP(mockVersion) + expect(result).toBe(true) + }) + + it('should return false when version is less than the supported MCP version', () => { + const mockVersion = '0.0.1' + const result = isSupportMCP(mockVersion) + expect(result).toBe(false) + }) + }) +}) diff --git a/web/utils/plugin-version-feature.ts b/web/utils/plugin-version-feature.ts new file mode 100644 index 0000000000..51d366bf9c --- /dev/null +++ b/web/utils/plugin-version-feature.ts @@ -0,0 +1,10 @@ +import { isEqualOrLaterThanVersion } from './semver' + +const SUPPORT_MCP_VERSION = '0.0.2' + +export const isSupportMCP = (version?: string): boolean => { + if (!version) + return false + + return isEqualOrLaterThanVersion(version, SUPPORT_MCP_VERSION) +} diff --git a/web/utils/semver.spec.ts b/web/utils/semver.spec.ts new file mode 100644 index 0000000000..c2188a976c --- /dev/null +++ b/web/utils/semver.spec.ts @@ -0,0 +1,75 @@ +import { compareVersion, getLatestVersion, isEqualOrLaterThanVersion } from './semver' + +describe('semver utilities', () => { + describe('getLatestVersion', () => { + it('should return the latest version from a list of versions', () => { + expect(getLatestVersion(['1.0.0', '1.1.0', '1.0.1'])).toBe('1.1.0') + expect(getLatestVersion(['2.0.0', '1.9.9', '1.10.0'])).toBe('2.0.0') + expect(getLatestVersion(['1.0.0-alpha', '1.0.0-beta', '1.0.0'])).toBe('1.0.0') + }) + + it('should handle patch versions correctly', () => { + expect(getLatestVersion(['1.0.1', '1.0.2', '1.0.0'])).toBe('1.0.2') + expect(getLatestVersion(['1.0.10', '1.0.9', '1.0.11'])).toBe('1.0.11') + }) + + it('should handle mixed version formats', () => { + expect(getLatestVersion(['v1.0.0', '1.1.0', 'v1.2.0'])).toBe('v1.2.0') + expect(getLatestVersion(['1.0.0-rc.1', '1.0.0', '1.0.0-beta'])).toBe('1.0.0') + }) + + it('should return the only version if only one version is provided', () => { + expect(getLatestVersion(['1.0.0'])).toBe('1.0.0') + }) + }) + + describe('compareVersion', () => { + it('should return 1 when first version is greater', () => { + expect(compareVersion('1.1.0', '1.0.0')).toBe(1) + expect(compareVersion('2.0.0', '1.9.9')).toBe(1) + expect(compareVersion('1.0.1', '1.0.0')).toBe(1) + }) + + it('should return -1 when first version is less', () => { + expect(compareVersion('1.0.0', '1.1.0')).toBe(-1) + expect(compareVersion('1.9.9', '2.0.0')).toBe(-1) + expect(compareVersion('1.0.0', '1.0.1')).toBe(-1) + }) + + it('should return 0 when versions are equal', () => { + expect(compareVersion('1.0.0', '1.0.0')).toBe(0) + expect(compareVersion('2.1.3', '2.1.3')).toBe(0) + }) + + it('should handle pre-release versions correctly', () => { + expect(compareVersion('1.0.0-beta', '1.0.0-alpha')).toBe(1) + expect(compareVersion('1.0.0', '1.0.0-beta')).toBe(1) + expect(compareVersion('1.0.0-alpha', '1.0.0-beta')).toBe(-1) + }) + }) + + describe('isEqualOrLaterThanVersion', () => { + it('should return true when baseVersion is greater than targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.1.0', '1.0.0')).toBe(true) + expect(isEqualOrLaterThanVersion('2.0.0', '1.9.9')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.1', '1.0.0')).toBe(true) + }) + + it('should return true when baseVersion is equal to targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.0')).toBe(true) + expect(isEqualOrLaterThanVersion('2.1.3', '2.1.3')).toBe(true) + }) + + it('should return false when baseVersion is less than targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.1.0')).toBe(false) + expect(isEqualOrLaterThanVersion('1.9.9', '2.0.0')).toBe(false) + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.1')).toBe(false) + }) + + it('should handle pre-release versions correctly', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.0-beta')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.0-beta', '1.0.0-alpha')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.0-alpha', '1.0.0')).toBe(false) + }) + }) +}) diff --git a/web/utils/semver.ts b/web/utils/semver.ts index f1b9eb8d7e..aea84153ec 100644 --- a/web/utils/semver.ts +++ b/web/utils/semver.ts @@ -7,3 +7,7 @@ export const getLatestVersion = (versionList: string[]) => { export const compareVersion = (v1: string, v2: string) => { return semver.compare(v1, v2) } + +export const isEqualOrLaterThanVersion = (baseVersion: string, targetVersion: string) => { + return semver.gte(baseVersion, targetVersion) +} diff --git a/web/utils/var.ts b/web/utils/var.ts index 06cb43c268..ce0ca030e1 100644 --- a/web/utils/var.ts +++ b/web/utils/var.ts @@ -1,4 +1,4 @@ -import { MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config' +import { MARKETPLACE_URL_PREFIX, MAX_VAR_KEY_LENGTH, VAR_ITEM_TEMPLATE, VAR_ITEM_TEMPLATE_IN_WORKFLOW, getMaxVarNameLength } from '@/config' import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, @@ -108,3 +108,25 @@ export const getVars = (value: string) => { // Set the value of basePath // example: /dify export const basePath = '' + +export function getMarketplaceUrl(path: string, params?: Record<string, string | undefined>) { + const searchParams = new URLSearchParams({ source: encodeURIComponent(window.location.origin) }) + if (params) { + Object.keys(params).forEach((key) => { + const value = params[key] + if (value !== undefined && value !== null) + searchParams.append(key, value) + }) + } + return `${MARKETPLACE_URL_PREFIX}${path}?${searchParams.toString()}` +} + +export const replaceSpaceWithUnderscreInVarNameInput = (input: HTMLInputElement) => { + const start = input.selectionStart + const end = input.selectionEnd + + input.value = input.value.replaceAll(' ', '_') + + if (start !== null && end !== null) + input.setSelectionRange(start, end) +}